You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@superset.apache.org by cc...@apache.org on 2018/04/18 07:08:31 UTC

[incubator-superset] branch chris--dashboard-perf created (now ae44001)

This is an automated email from the ASF dual-hosted git repository.

ccwilliams pushed a change to branch chris--dashboard-perf
in repository https://gitbox.apache.org/repos/asf/incubator-superset.git.


      at ae44001  [dashboard builder] address major perf + css issues

This branch includes the following new commits:

     new ae44001  [dashboard builder] address major perf + css issues

The 1 revisions listed above as "new" are entirely new to this
repository and will be described in separate emails.  The revisions
listed as "add" were already present in the repository and have only
been added to this reference.


-- 
To stop receiving notification emails like this one, please contact
ccwilliams@apache.org.

[incubator-superset] 01/01: [dashboard builder] address major perf + css issues

Posted by cc...@apache.org.
This is an automated email from the ASF dual-hosted git repository.

ccwilliams pushed a commit to branch chris--dashboard-perf
in repository https://gitbox.apache.org/repos/asf/incubator-superset.git

commit ae44001347bb7fd4ba013487c7552864390156cd
Author: Chris Williams <ch...@airbnb.com>
AuthorDate: Wed Apr 18 00:08:01 2018 -0700

    [dashboard builder] address major perf + css issues
---
 superset/assets/images/loading.gif                 | Bin 16671 -> 1945878 bytes
 superset/assets/javascripts/chart/Chart.jsx        |  85 +++++----
 .../assets/javascripts/chart/ChartContainer.jsx    |  22 +--
 superset/assets/javascripts/chart/chartReducer.js  |   5 +-
 superset/assets/javascripts/components/Loading.jsx |   3 +
 .../javascripts/dashboard/components/Dashboard.jsx | 175 ++++++++---------
 .../dashboard/components/DashboardContainer.jsx    |   1 +
 .../javascripts/dashboard/components/GridCell.jsx  |  20 +-
 .../dashboard/components/GridLayout.jsx            | 178 +++++++++---------
 .../dashboard/components/SliceAdder.jsx            |   2 +-
 .../dashboard/components/SliceHeader.jsx           |  17 +-
 .../dashboard/util/dashboardLayoutConverter.js     |  29 +--
 .../dashboard/v2/actions/dashboardLayout.js        |  20 +-
 .../v2/components/BuilderComponentPane.jsx         |   7 +-
 .../dashboard/v2/components/DashboardBuilder.jsx   |   4 -
 .../dashboard/v2/components/DashboardGrid.jsx      | 126 +++++++------
 .../dashboard/v2/components/WithKeyListener.jsx    |  55 ++++++
 .../dashboard/v2/components/dnd/DragDroppable.jsx  |   4 +-
 .../v2/components/gridComponents/Chart.jsx         | 206 +++++++++++++++++++++
 .../v2/components/gridComponents/ChartHolder.jsx   |  53 +++---
 .../v2/components/gridComponents/Column.jsx        |  29 ++-
 .../dashboard/v2/components/gridComponents/Row.jsx |  29 ++-
 .../v2/components/menu/WithPopoverMenu.jsx         |   7 +-
 .../v2/components/resizable/ResizableContainer.jsx |  25 +--
 .../javascripts/dashboard/v2/containers/Chart.jsx  |  44 +++++
 .../dashboard/v2/containers/DashboardBuilder.jsx   |   3 +-
 .../dashboard/v2/containers/DashboardComponent.jsx |  17 +-
 .../dashboard/v2/containers/DashboardGrid.jsx      |   3 +-
 .../dashboard/v2/reducers/dashboardLayout.js       |  12 +-
 .../javascripts/dashboard/v2/reducers/index.js     |  25 ++-
 .../dashboard/v2/stylesheets/components/chart.less |  31 +++-
 .../v2/stylesheets/components/column.less          |   5 +-
 .../dashboard/v2/stylesheets/components/row.less   |   5 +-
 .../javascripts/dashboard/v2/stylesheets/dnd.less  |   2 +-
 .../javascripts/dashboard/v2/stylesheets/grid.less |  14 +-
 .../dashboard/v2/stylesheets/resizable.less        |  26 ++-
 .../v2/util/charts/getEffectiveExtraFilters.js     |  41 ++++
 .../v2/util/charts/getFormDataWithExtraFilters.js  |  40 ++++
 .../dashboard/v2/util/dropOverflowsParent.js       |  12 +-
 .../dashboard/v2/util/getDropPosition.js           |   2 +-
 .../explore/components/ExploreChartPanel.jsx       |  14 +-
 superset/assets/package.json                       |   2 +-
 superset/assets/stylesheets/dashboard.less         |  20 +-
 superset/assets/visualizations/nvd3_vis.css        |   5 -
 superset/templates/superset/dashboard.html         |   7 +-
 45 files changed, 940 insertions(+), 492 deletions(-)

diff --git a/superset/assets/images/loading.gif b/superset/assets/images/loading.gif
index 01ae393..ae5cbdd 100644
Binary files a/superset/assets/images/loading.gif and b/superset/assets/images/loading.gif differ
diff --git a/superset/assets/javascripts/chart/Chart.jsx b/superset/assets/javascripts/chart/Chart.jsx
index 78a9175..b223c9e 100644
--- a/superset/assets/javascripts/chart/Chart.jsx
+++ b/superset/assets/javascripts/chart/Chart.jsx
@@ -5,7 +5,6 @@ import Mustache from 'mustache';
 import { Tooltip } from 'react-bootstrap';
 
 import { d3format } from '../modules/utils';
-import { chartPropType } from './chartReducer';
 import ChartBody from './ChartBody';
 import Loading from '../components/Loading';
 import { Logger, LOG_ACTIONS_RENDER_EVENT } from '../logger';
@@ -18,7 +17,7 @@ import './chart.css';
 const propTypes = {
   annotationData: PropTypes.object,
   actions: PropTypes.object,
-  chart: PropTypes.shape(chartPropType).isRequired,
+  sliceId: PropTypes.string.isRequired,
   containerId: PropTypes.string.isRequired,
   datasource: PropTypes.object.isRequired,
   formData: PropTypes.object,
@@ -62,7 +61,7 @@ class Chart extends React.PureComponent {
     this.annotationData = props.annotationData;
     this.containerId = props.containerId;
     this.selector = `#${this.containerId}`;
-    this.formData = props.formData || props.chart.formData;
+    this.formData = props.formData;
     this.datasource = props.datasource;
     this.addFilter = this.addFilter.bind(this);
     this.getFilters = this.getFilters.bind(this);
@@ -73,12 +72,15 @@ class Chart extends React.PureComponent {
   }
 
   componentDidMount() {
-    const formData = this.props.formData || this.props.chart.formData;
     if (this.props.triggerQuery) {
+      const formData = this.props.formData;
       this.props.actions.runQuery(formData, false,
         this.props.timeout,
-        this.props.chart.chartKey,
+        this.props.sliceId,
       );
+    } else {
+      // when drag/dropping in a dashboard, a chart may be unmounted/remounted but still have data
+      this.renderViz();
     }
   }
 
@@ -86,7 +88,7 @@ class Chart extends React.PureComponent {
     this.annotationData = nextProps.annotationData;
     this.containerId = nextProps.containerId;
     this.selector = `#${this.containerId}`;
-    this.formData = nextProps.formData || nextProps.chart.formData;
+    this.formData = nextProps.formData;
     this.datasource = nextProps.datasource;
   }
 
@@ -95,11 +97,12 @@ class Chart extends React.PureComponent {
         this.props.queryResponse &&
         ['success', 'rendered'].indexOf(this.props.chartStatus) > -1 &&
         !this.props.queryResponse.error && (
-        prevProps.annotationData !== this.props.annotationData ||
-        prevProps.queryResponse !== this.props.queryResponse ||
-        prevProps.height !== this.props.height ||
-        prevProps.width !== this.props.width ||
-        prevProps.lastRendered !== this.props.lastRendered)
+          prevProps.annotationData !== this.props.annotationData ||
+          prevProps.queryResponse !== this.props.queryResponse ||
+          prevProps.height !== this.props.height ||
+          prevProps.width !== this.props.width ||
+          prevProps.lastRendered !== this.props.lastRendered
+        )
     ) {
       this.renderViz();
     }
@@ -128,7 +131,8 @@ class Chart extends React.PureComponent {
   }
 
   width() {
-    return this.props.width || this.container.el.offsetWidth;
+    return this.props.width ||
+      (this.container && this.container.el && this.container.el.offsetWidth);
   }
 
   headerHeight() {
@@ -136,7 +140,8 @@ class Chart extends React.PureComponent {
   }
 
   height() {
-    return this.props.height || this.container.el.offsetHeight;
+    return this.props.height
+      || (this.container && this.container.el && this.container.el.offsetHeight);
   }
 
   d3format(col, number) {
@@ -156,7 +161,6 @@ class Chart extends React.PureComponent {
 
   renderTooltip() {
     if (this.state.tooltip) {
-      /* eslint-disable react/no-danger */
       return (
         <Tooltip
           className="chart-tooltip"
@@ -166,55 +170,54 @@ class Chart extends React.PureComponent {
           positionLeft={this.state.tooltip.x + 30}
           arrowOffsetTop={10}
         >
-          <div dangerouslySetInnerHTML={{ __html: this.state.tooltip.content }} />
+          <div // eslint-disable-next-line react/no-danger
+            dangerouslySetInnerHTML={{ __html: this.state.tooltip.content }}
+          />
         </Tooltip>
       );
-      /* eslint-enable react/no-danger */
     }
     return null;
   }
 
   renderViz() {
-    const viz = visMap[this.props.vizType];
-    // allow props.formData overwrite chart's own formData
-    const fd = this.props.formData || this.props.chart.formData;
-    const qr = this.props.queryResponse;
+    const { vizType, formData, queryResponse, setControlValue, sliceId, chartStatus } = this.props;
+    const visRenderer = visMap[vizType];
     const renderStart = Logger.getTimestamp();
     try {
       // Executing user-defined data mutator function
-      if (fd.js_data) {
-        qr.data = sandboxedEval(fd.js_data)(qr.data);
+      if (formData.js_data) {
+        queryResponse.data = sandboxedEval(formData.js_data)(queryResponse.data);
+      }
+      visRenderer(this, queryResponse, setControlValue);
+      if (chartStatus !== 'rendered') {
+        this.props.actions.chartRenderingSucceeded(sliceId);
       }
-      // [re]rendering the visualization
-      viz(this, qr, this.props.setControlValue);
       Logger.append(LOG_ACTIONS_RENDER_EVENT, {
-        label: this.props.chart.chartKey,
-        vis_type: this.props.vizType,
+        label: sliceId,
+        vis_type: vizType,
         start_offset: renderStart,
         duration: Logger.getTimestamp() - renderStart,
       });
-      this.props.actions.chartRenderingSucceeded(this.props.chart.chartKey);
     } catch (e) {
-      console.error(e);  // eslint-disable-line
-      this.props.actions.chartRenderingFailed(e, this.props.chart.chartKey);
+      console.error(e); // eslint-disable-line no-console
+      this.props.actions.chartRenderingFailed(e, sliceId);
     }
   }
 
   render() {
     const isLoading = this.props.chartStatus === 'loading';
+
+    // this allows <Loading /> to be positioned in the middle of the chart
+    const containerStyles = isLoading ? { height: this.height(), width: this.width() } : null;
     return (
-      <div className={`token col-md-12 ${isLoading ? 'is-loading' : ''}`}
-      >
+      <div className={`chart-container ${isLoading ? 'is-loading' : ''}`} style={containerStyles}>
         {this.renderTooltip()}
-        {isLoading &&
-          <Loading size={25} />
-        }
+        {isLoading && <Loading size={75} />}
         {this.props.chartAlert &&
-        <StackTraceMessage
-          message={this.props.chartAlert}
-          queryResponse={this.props.queryResponse}
-        />
-        }
+          <StackTraceMessage
+            message={this.props.chartAlert}
+            queryResponse={this.props.queryResponse}
+          />}
 
         {!isLoading &&
           !this.props.chartAlert &&
@@ -225,8 +228,8 @@ class Chart extends React.PureComponent {
             width={this.width()}
             onQuery={this.props.onQuery}
             onDismiss={this.props.onDismissRefreshOverlay}
-          />
-        }
+          />}
+
         {!isLoading && !this.props.chartAlert &&
           <ChartBody
             containerId={this.containerId}
diff --git a/superset/assets/javascripts/chart/ChartContainer.jsx b/superset/assets/javascripts/chart/ChartContainer.jsx
index ff072dc..b66fe5d 100644
--- a/superset/assets/javascripts/chart/ChartContainer.jsx
+++ b/superset/assets/javascripts/chart/ChartContainer.jsx
@@ -1,29 +1,13 @@
 import { connect } from 'react-redux';
 import { bindActionCreators } from 'redux';
 
-import * as Actions from './chartAction';
+import * as actions from './chartAction';
 import Chart from './Chart';
 
-function mapStateToProps({}, ownProps) {
-  const chart = ownProps.chart;
-  return {
-    annotationData: chart.annotationData,
-    chartAlert: chart.chartAlert,
-    chartStatus: chart.chartStatus,
-    chartUpdateEndTime: chart.chartUpdateEndTime,
-    chartUpdateStartTime: chart.chartUpdateStartTime,
-    latestQueryFormData: chart.latestQueryFormData,
-    lastRendered: chart.lastRendered,
-    queryResponse: chart.queryResponse,
-    queryRequest: chart.queryRequest,
-    triggerQuery: chart.triggerQuery,
-  };
-}
-
 function mapDispatchToProps(dispatch) {
   return {
-    actions: bindActionCreators(Actions, dispatch),
+    actions: bindActionCreators(actions, dispatch),
   };
 }
 
-export default connect(mapStateToProps, mapDispatchToProps)(Chart);
+export default connect(null, mapDispatchToProps)(Chart);
diff --git a/superset/assets/javascripts/chart/chartReducer.js b/superset/assets/javascripts/chart/chartReducer.js
index 5c3ad59..edf4e85 100644
--- a/superset/assets/javascripts/chart/chartReducer.js
+++ b/superset/assets/javascripts/chart/chartReducer.js
@@ -159,7 +159,10 @@ export default function chartReducer(charts = {}, action) {
   }
 
   if (action.type in actionHandlers) {
-    return { ...charts, [action.key]: actionHandlers[action.type](charts[action.key], action) };
+    return {
+      ...charts,
+      [action.key]: actionHandlers[action.type](charts[action.key], action),
+    };
   }
 
   return charts;
diff --git a/superset/assets/javascripts/components/Loading.jsx b/superset/assets/javascripts/components/Loading.jsx
index 416e770..810c581 100644
--- a/superset/assets/javascripts/components/Loading.jsx
+++ b/superset/assets/javascripts/components/Loading.jsx
@@ -20,6 +20,9 @@ export default function Loading(props) {
         padding: 0,
         margin: 0,
         position: 'absolute',
+        left: '50%',
+        top: '50%',
+        transform: 'translate(-50%, -60%)',
       }}
     />
   );
diff --git a/superset/assets/javascripts/dashboard/components/Dashboard.jsx b/superset/assets/javascripts/dashboard/components/Dashboard.jsx
index cd5fee6..292c359 100644
--- a/superset/assets/javascripts/dashboard/components/Dashboard.jsx
+++ b/superset/assets/javascripts/dashboard/components/Dashboard.jsx
@@ -2,9 +2,10 @@ import React from 'react';
 import PropTypes from 'prop-types';
 
 import AlertsWrapper from '../../components/AlertsWrapper';
-import GridLayout from './GridLayout';
+import DashboardBuilder from '../v2/containers/DashboardBuilder';
+// import GridLayout from './GridLayout';
 import { slicePropShape } from '../reducers/propShapes';
-import { exportChart } from '../../explore/exploreUtils';
+// import { exportChart } from '../../explore/exploreUtils';
 import { areObjectsEqual } from '../../reduxUtils';
 import { Logger, ActionLog, LOG_ACTIONS_PAGE_LOAD,
   LOG_ACTIONS_LOAD_EVENT, LOG_ACTIONS_RENDER_EVENT } from '../../logger';
@@ -18,7 +19,7 @@ const propTypes = {
   initMessages: PropTypes.array,
   dashboard: PropTypes.object.isRequired,
   charts: PropTypes.object.isRequired,
-  slices:  PropTypes.objectOf(slicePropShape).isRequired,
+  slices: PropTypes.objectOf(slicePropShape).isRequired,
   datasources: PropTypes.object.isRequired,
   layout: PropTypes.object.isRequired,
   filters: PropTypes.object,
@@ -57,22 +58,23 @@ class Dashboard extends React.PureComponent {
     });
     Logger.start(this.loadingLog);
 
-    this.rerenderCharts = this.rerenderCharts.bind(this);
-    this.getFormDataExtra = this.getFormDataExtra.bind(this);
-    this.exploreChart = this.exploreChart.bind(this);
-    this.exportCSV = this.exportCSV.bind(this);
+    // this.rerenderCharts = this.rerenderCharts.bind(this);
+    // this.getFormDataExtra = this.getFormDataExtra.bind(this);
+    // this.exploreChart = this.exploreChart.bind(this);
+    // this.exportCSV = this.exportCSV.bind(this);
 
-    this.props.actions.saveSliceName = this.props.actions.saveSliceName.bind(this);
-    this.props.actions.removeSliceFromDashboard =
-      this.props.actions.removeSliceFromDashboard.bind(this);
-    this.props.actions.toggleExpandSlice =
-      this.props.actions.toggleExpandSlice.bind(this);
-    this.props.actions.addFilter = this.props.actions.addFilter.bind(this);
-    this.props.actions.removeFilter = this.props.actions.removeFilter.bind(this);
+    // this.props.actions.saveSliceName = this.props.actions.saveSliceName.bind(this);
+    // this.props.actions.removeSliceFromDashboard =
+      // this.props.actions.removeSliceFromDashboard.bind(this);
+    // this.props.actions.toggleExpandSlice =
+      // this.props.actions.toggleExpandSlice.bind(this);
+    // this.props.actions.addFilter = this.props.actions.addFilter.bind(this);
+    // this.props.actions.removeFilter = this.props.actions.removeFilter.bind(this);
   }
 
   componentDidMount() {
-    window.addEventListener('resize', this.rerenderCharts);
+    // grid does this now
+    // window.addEventListener('resize', this.rerenderCharts);
   }
 
   componentWillReceiveProps(nextProps) {
@@ -126,7 +128,7 @@ class Dashboard extends React.PureComponent {
   }
 
   componentWillUnmount() {
-    window.removeEventListener('resize', this.rerenderCharts);
+    // window.removeEventListener('resize', this.rerenderCharts);
   }
 
   onBeforeUnload(hasChanged) {
@@ -142,12 +144,12 @@ class Dashboard extends React.PureComponent {
     return Object.values(this.props.charts);
   }
 
-  getFormDataExtra(chart) {
-    const formDataExtra = Object.assign({}, chart.formData);
-    const extraFilters = this.effectiveExtraFilters(chart.slice_id);
-    formDataExtra.extra_filters = formDataExtra.filters.concat(extraFilters);
-    return formDataExtra;
-  }
+  // getFormDataExtra(chart) {
+  //   const formDataExtra = Object.assign({}, chart.formData);
+  //   const extraFilters = this.effectiveExtraFilters(chart.slice_id);
+  //   formDataExtra.extra_filters = formDataExtra.filters.concat(extraFilters);
+  //   return formDataExtra;
+  // }
 
   getFilters(sliceId) {
     return this.props.filters[sliceId];
@@ -169,41 +171,41 @@ class Dashboard extends React.PureComponent {
     return message; // Gecko + Webkit, Safari, Chrome etc.
   }
 
-  effectiveExtraFilters(sliceId) {
-    const metadata = this.props.dashboard.metadata;
-    const filters = this.props.filters;
-    const f = [];
-    const immuneSlices = metadata.filter_immune_slices || [];
-    if (sliceId && immuneSlices.includes(sliceId)) {
-      // The slice is immune to dashboard filters
-      return f;
-    }
-
-    // Building a list of fields the slice is immune to filters on
-    let immuneToFields = [];
-    if (
-      sliceId &&
-      metadata.filter_immune_slice_fields &&
-      metadata.filter_immune_slice_fields[sliceId]) {
-      immuneToFields = metadata.filter_immune_slice_fields[sliceId];
-    }
-    for (const filteringSliceId in filters) {
-      if (filteringSliceId === sliceId.toString()) {
-        // Filters applied by the slice don't apply to itself
-        continue;
-      }
-      for (const field in filters[filteringSliceId]) {
-        if (!immuneToFields.includes(field)) {
-          f.push({
-            col: field,
-            op: 'in',
-            val: filters[filteringSliceId][field],
-          });
-        }
-      }
-    }
-    return f;
-  }
+  // effectiveExtraFilters(sliceId) {
+  //   const metadata = this.props.dashboard.metadata;
+  //   const filters = this.props.filters;
+  //   const f = [];
+  //   const immuneSlices = metadata.filter_immune_slices || [];
+  //   if (sliceId && immuneSlices.includes(sliceId)) {
+  //     // The slice is immune to dashboard filters
+  //     return f;
+  //   }
+  //
+  //   // Building a list of fields the slice is immune to filters on
+  //   let immuneToFields = [];
+  //   if (
+  //     sliceId &&
+  //     metadata.filter_immune_slice_fields &&
+  //     metadata.filter_immune_slice_fields[sliceId]) {
+  //     immuneToFields = metadata.filter_immune_slice_fields[sliceId];
+  //   }
+  //   for (const filteringSliceId in filters) {
+  //     if (filteringSliceId === sliceId.toString()) {
+  //       // Filters applied by the slice don't apply to itself
+  //       continue;
+  //     }
+  //     for (const field in filters[filteringSliceId]) {
+  //       if (!immuneToFields.includes(field)) {
+  //         f.push({
+  //           col: field,
+  //           op: 'in',
+  //           val: filters[filteringSliceId][field],
+  //         });
+  //       }
+  //     }
+  //   }
+  //   return f;
+  // }
 
   refreshExcept(filterKey) {
     const immune = this.props.dashboard.metadata.filter_immune_slices || [];
@@ -220,26 +222,26 @@ class Dashboard extends React.PureComponent {
     });
   }
 
-  exploreChart(chartKey) {
-    const chart = this.props.charts[chartKey];
-    const formData = this.getFormDataExtra(chart);
-    exportChart(formData);
-  }
-
-  exportCSV(chartKey) {
-    const chart = this.props.charts[chartKey];
-    const formData = this.getFormDataExtra(chart);
-    exportChart(formData, 'csv');
-  }
+  // exploreChart(chartKey) {
+  //   const chart = this.props.charts[chartKey];
+  //   const formData = this.getFormDataExtra(chart);
+  //   exportChart(formData);
+  // }
+  //
+  // exportCSV(chartKey) {
+  //   const chart = this.props.charts[chartKey];
+  //   const formData = this.getFormDataExtra(chart);
+  //   exportChart(formData, 'csv');
+  // }
 
   // re-render chart without fetch
-  rerenderCharts() {
-    this.getAllCharts().forEach((chart) => {
-      setTimeout(() => {
-        this.props.actions.renderTriggered(new Date().getTime(), chart.chartKey);
-      }, 50);
-    });
-  }
+  // rerenderCharts() {
+  //   this.getAllCharts().forEach((chart) => {
+  //     setTimeout(() => {
+  //       this.props.actions.renderTriggered(new Date().getTime(), chart.chartKey);
+  //     }, 50);
+  //   });
+  // }
 
   render() {
     return (
@@ -247,27 +249,8 @@ class Dashboard extends React.PureComponent {
         <div id="dashboard-header">
           <AlertsWrapper initMessages={this.props.initMessages} />
         </div>
-        <GridLayout
-            dashboard={this.props.dashboard}
-            layout={this.props.layout}
-            datasources={this.props.datasources}
-            slices={this.props.slices}
-            filters={this.props.filters}
-            charts={this.props.charts}
-            timeout={this.props.timeout}
-            onChange={this.onChange}
-            rerenderCharts={this.rerenderCharts}
-            getFormDataExtra={this.getFormDataExtra}
-            exploreChart={this.exploreChart}
-            exportCSV={this.exportCSV}
-            refreshChart={this.props.actions.refreshChart}
-            saveSliceName={this.props.actions.saveSliceName}
-            toggleExpandSlice={this.props.actions.toggleExpandSlice}
-            addFilter={this.props.actions.addFilter}
-            getFilters={this.getFilters}
-            removeFilter={this.props.actions.removeFilter}
-            editMode={this.props.editMode}
-          />
+
+        <DashboardBuilder />
       </div>
     );
   }
diff --git a/superset/assets/javascripts/dashboard/components/DashboardContainer.jsx b/superset/assets/javascripts/dashboard/components/DashboardContainer.jsx
index 7140655..858fc27 100644
--- a/superset/assets/javascripts/dashboard/components/DashboardContainer.jsx
+++ b/superset/assets/javascripts/dashboard/components/DashboardContainer.jsx
@@ -6,6 +6,7 @@ import { saveSliceName } from '../actions/allSlices';
 import * as chartActions from '../../chart/chartAction';
 import Dashboard from './Dashboard';
 
+// @TODO remove unneeded actionsn + props
 function mapStateToProps({ datasources, allSlices, charts, dashboard, dashboardLayout, impressionId }) {
   return {
     initMessages: dashboard.common.flash_messages,
diff --git a/superset/assets/javascripts/dashboard/components/GridCell.jsx b/superset/assets/javascripts/dashboard/components/GridCell.jsx
index c3afe27..e992236 100644
--- a/superset/assets/javascripts/dashboard/components/GridCell.jsx
+++ b/superset/assets/javascripts/dashboard/components/GridCell.jsx
@@ -19,7 +19,7 @@ const propTypes = {
   slice: slicePropShape.isRequired,
   chart: PropTypes.shape(chartPropType).isRequired,
   formData: PropTypes.object,
-  filters: PropTypes.object,
+  // filters: PropTypes.object,
   refreshChart: PropTypes.func,
   updateSliceName: PropTypes.func,
   toggleExpandSlice: PropTypes.func,
@@ -90,10 +90,20 @@ class GridCell extends React.PureComponent {
 
   render() {
     const {
-      isExpanded, isLoading, isCached, cachedDttm,
-      updateSliceName, toggleExpandSlice,
-      chart, slice, datasource, formData, timeout, annotationQuery,
-      exploreChart, exportCSV,
+      isExpanded,
+      isLoading,
+      isCached,
+      cachedDttm,
+      updateSliceName,
+      toggleExpandSlice,
+      chart,
+      slice,
+      datasource,
+      formData,
+      timeout,
+      annotationQuery,
+      exploreChart,
+      exportCSV,
     } = this.props;
 
     return (
diff --git a/superset/assets/javascripts/dashboard/components/GridLayout.jsx b/superset/assets/javascripts/dashboard/components/GridLayout.jsx
index d01a3ab..87dd4b5 100644
--- a/superset/assets/javascripts/dashboard/components/GridLayout.jsx
+++ b/superset/assets/javascripts/dashboard/components/GridLayout.jsx
@@ -1,8 +1,8 @@
 import React from 'react';
 import PropTypes from 'prop-types';
-import cx from 'classnames';
+// import cx from 'classnames';
 
-import GridCell from './GridCell';
+// import GridCell from './GridCell';
 import { slicePropShape } from '../reducers/propShapes';
 import DashboardBuilder from '../v2/containers/DashboardBuilder';
 
@@ -49,97 +49,99 @@ class GridLayout extends React.Component {
       this.updateSliceName.bind(this) : null;
   }
 
-  getWidgetId(sliceId) {
-    return 'widget_' + sliceId;
-  }
+  // getWidgetId(sliceId) {
+  //   return 'widget_' + sliceId;
+  // }
 
-  getWidgetHeight(sliceId) {
-    const widgetId = this.getWidgetId(sliceId);
-    if (!widgetId || !this.refs[widgetId]) {
-      return 400;
-    }
-    return this.refs[widgetId].parentNode.clientHeight;
-  }
+  // getWidgetHeight(sliceId) {
+  //   const widgetId = this.getWidgetId(sliceId);
+  //   if (!widgetId || !this.refs[widgetId]) {
+  //     return 400;
+  //   }
+  //   return this.refs[widgetId].parentNode.clientHeight;
+  // }
+  //
+  // getWidgetWidth(sliceId) {
+  //   const widgetId = this.getWidgetId(sliceId);
+  //   if (!widgetId || !this.refs[widgetId]) {
+  //     return 400;
+  //   }
+  //   return this.refs[widgetId].parentNode.clientWidth;
+  // }
+  //
+  // // updateSliceName(sliceId, sliceName) {
+  // //   const key = 'slice_' + sliceId;
+  // //   const currentSlice = this.props.slices[key];
+  // //   if (!currentSlice || currentSlice.slice_name === sliceName) {
+  // //     return;
+  // //   }
+  // //
+  // //   this.props.saveSliceName(currentSlice, sliceName);
+  // // }
 
-  getWidgetWidth(sliceId) {
-    const widgetId = this.getWidgetId(sliceId);
-    if (!widgetId || !this.refs[widgetId]) {
-      return 400;
-    }
-    return this.refs[widgetId].parentNode.clientWidth;
-  }
+  // isExpanded(sliceId) {
+  //   return this.props.dashboard.metadata.expanded_slices &&
+  //     this.props.dashboard.metadata.expanded_slices[sliceId];
+  // }
 
-  updateSliceName(sliceId, sliceName) {
-    const key = 'slice_' + sliceId;
-    const currentSlice = this.props.slices[key];
-    if (!currentSlice || currentSlice.slice_name === sliceName) {
-      return;
-    }
-
-    this.props.saveSliceName(currentSlice, sliceName);
-  }
-
-  isExpanded(sliceId) {
-    return this.props.dashboard.metadata.expanded_slices &&
-      this.props.dashboard.metadata.expanded_slices[sliceId];
-  }
-
-  componentDidUpdate(prevProps) {
-    if (prevProps.editMode !== this.props.editMode) {
-      this.props.rerenderCharts();
-    }
-  }
+  // componentDidUpdate(prevProps) {
+  //   if (prevProps.editMode !== this.props.editMode) {
+  //     this.props.rerenderCharts();
+  //   }
+  // }
   render() {
-    const cells = {};
-    this.props.dashboard.sliceIds.forEach((sliceId) => {
-      const key = `slice_${sliceId}`;
-      const currentChart = this.props.charts[key];
-      const currentSlice = this.props.slices[key];
-      if (currentChart) {
-        const currentDatasource = this.props.datasources[currentChart.form_data.datasource];
-        const queryResponse = currentChart.queryResponse || {};
-        cells[key] = (
-          <div
-            id={key}
-            key={sliceId}
-            className={cx('widget', `${currentSlice.viz_type}`, { 'is-edit': this.props.editMode })}
-            ref={this.getWidgetId(sliceId)}
-          >
-            <GridCell
-              slice={currentSlice}
-              chart={currentChart}
-              datasource={currentDatasource}
-              filters={this.props.filters}
-              formData={this.props.getFormDataExtra(currentChart)}
-              timeout={this.props.timeout}
-              widgetHeight={this.getWidgetHeight(sliceId)}
-              widgetWidth={this.getWidgetWidth(sliceId)}
-              exploreChart={this.props.exploreChart}
-              exportCSV={this.props.exportCSV}
-              isExpanded={!!this.isExpanded(sliceId)}
-              isLoading={currentChart.chartStatus === 'loading'}
-              isCached={queryResponse.is_cached}
-              cachedDttm={queryResponse.cached_dttm}
-              toggleExpandSlice={this.props.toggleExpandSlice}
-              refreshChart={this.props.refreshChart}
-              updateSliceName={this.updateSliceName}
-              addFilter={this.props.addFilter}
-              getFilters={this.props.getFilters}
-              removeFilter={this.props.removeFilter}
-              editMode={this.props.editMode}
-              annotationQuery={currentChart.annotationQuery}
-              annotationError={currentChart.annotationError}
-            />
-          </div>
-        );
-      }
-    });
+    return <DashboardBuilder />;
 
-    return (
-      <DashboardBuilder
-        cells={cells}
-      />
-    );
+    // const cells = {};
+    // this.props.dashboard.sliceIds.forEach((sliceId) => {
+    //   const key = `slice_${sliceId}`;
+    //   const currentChart = this.props.charts[key];
+    //   const currentSlice = this.props.slices[key];
+    //   if (currentChart) {
+    //     const currentDatasource = this.props.datasources[currentChart.form_data.datasource];
+    //     const queryResponse = currentChart.queryResponse || {};
+    //     cells[key] = (
+    //       <div
+    //         id={key}
+    //         key={sliceId}
+    //         className={cx('widget', `${currentSlice.viz_type}`, { 'is-edit': this.props.editMode })}
+    //         ref={this.getWidgetId(sliceId)}
+    //       >
+    //         <GridCell
+    //           slice={currentSlice}
+    //           chart={currentChart}
+    //           datasource={currentDatasource}
+    //           filters={this.props.filters}
+    //           formData={this.props.getFormDataExtra(currentChart)}
+    //           timeout={this.props.timeout}
+    //           widgetHeight={this.getWidgetHeight(sliceId)}
+    //           widgetWidth={this.getWidgetWidth(sliceId)}
+    //           exploreChart={this.props.exploreChart}
+    //           exportCSV={this.props.exportCSV}
+    //           isExpanded={!!this.isExpanded(sliceId)}
+    //           isLoading={currentChart.chartStatus === 'loading'}
+    //           isCached={queryResponse.is_cached}
+    //           cachedDttm={queryResponse.cached_dttm}
+    //           toggleExpandSlice={this.props.toggleExpandSlice}
+    //           refreshChart={this.props.refreshChart}
+    //           updateSliceName={this.updateSliceName}
+    //           addFilter={this.props.addFilter}
+    //           getFilters={this.props.getFilters}
+    //           removeFilter={this.props.removeFilter}
+    //           editMode={this.props.editMode}
+    //           annotationQuery={currentChart.annotationQuery}
+    //           annotationError={currentChart.annotationError}
+    //         />
+    //       </div>
+    //     );
+    //   }
+    // });
+    //
+    // return (
+    //   <DashboardBuilder
+    //     cells={cells}
+    //   />
+    // );
   }
 }
 
diff --git a/superset/assets/javascripts/dashboard/components/SliceAdder.jsx b/superset/assets/javascripts/dashboard/components/SliceAdder.jsx
index 2a1e983..11be188 100644
--- a/superset/assets/javascripts/dashboard/components/SliceAdder.jsx
+++ b/superset/assets/javascripts/dashboard/components/SliceAdder.jsx
@@ -123,6 +123,7 @@ class SliceAdder extends React.Component {
 
     return (
       <DragDroppable
+        key={key}
         component={{ type, id, meta }}
         parentComponent={{ id: NEW_COMPONENTS_SOURCE_ID, type: NEW_COMPONENT_SOURCE_TYPE }}
         index={0}
@@ -134,7 +135,6 @@ class SliceAdder extends React.Component {
       <div
         ref={dragSourceRef}
         className="chart-card-container"
-        key={key}
         style={style}
       >
         <div className={cx('chart-card', { 'is-selected': isSelected })}>
diff --git a/superset/assets/javascripts/dashboard/components/SliceHeader.jsx b/superset/assets/javascripts/dashboard/components/SliceHeader.jsx
index 264542a..d291b3b 100644
--- a/superset/assets/javascripts/dashboard/components/SliceHeader.jsx
+++ b/superset/assets/javascripts/dashboard/components/SliceHeader.jsx
@@ -7,6 +7,7 @@ import TooltipWrapper from '../../components/TooltipWrapper';
 import SliceHeaderControls from './SliceHeaderControls';
 
 const propTypes = {
+  innerRef: PropTypes.func,
   slice: PropTypes.object.isRequired,
   isExpanded: PropTypes.bool,
   isCached: PropTypes.bool,
@@ -22,6 +23,7 @@ const propTypes = {
 };
 
 const defaultProps = {
+  innerRef: null,
   forceRefresh: () => ({}),
   removeSlice: () => ({}),
   updateSliceName: () => ({}),
@@ -46,15 +48,22 @@ class SliceHeader extends React.PureComponent {
 
   render() {
     const {
-      slice, isExpanded, isCached, cachedDttm,
-      toggleExpandSlice, forceRefresh,
-      exploreChart, exportCSV,
+      slice,
+      isExpanded,
+      isCached,
+      cachedDttm,
+      toggleExpandSlice,
+      forceRefresh,
+      exploreChart,
+      exportCSV,
+      innerRef,
     } = this.props;
+
     const annoationsLoading = t('Annotation layers are still loading.');
     const annoationsError = t('One ore more annotation layers failed loading.');
 
     return (
-      <div className="row chart-header">
+      <div className="chart-header" ref={innerRef}>
         <div className="col-md-12">
           <div className="header">
             <EditableTitle
diff --git a/superset/assets/javascripts/dashboard/util/dashboardLayoutConverter.js b/superset/assets/javascripts/dashboard/util/dashboardLayoutConverter.js
index c58e792..a3f6f0a 100644
--- a/superset/assets/javascripts/dashboard/util/dashboardLayoutConverter.js
+++ b/superset/assets/javascripts/dashboard/util/dashboardLayoutConverter.js
@@ -93,18 +93,28 @@ function getChartHolder(item) {
   };
 }
 
-function getChildrenMax(items, attr, layout) {
-  return Math.max.apply(null, items.map(child => {
-    return layout[child].meta[attr];
-  }));
-}
-
 function getChildrenSum(items, attr, layout) {
   return items.reduce((preValue, child) => {
     return preValue + layout[child].meta[attr];
   }, 0);
 }
 
+function getChildrenMax(items, attr, layout) {
+  return Math.max.apply(null, items.map((childId) => {
+    const child = layout[childId];
+    if (child.type === ROW_TYPE && attr === 'width') {
+      // rows don't have widths themselves
+      return getChildrenSum(child.children, attr, layout);
+    } else if (child.type === COLUMN_TYPE && attr === 'height') {
+      // columns don't have heights themselves
+      return getChildrenSum(child.children, attr, layout);
+    }
+
+    return child.meta[attr];
+  }));
+}
+
+
 function sortByRowId(item1, item2) {
   return item1.row - item2.row;
 }
@@ -232,17 +242,15 @@ function doConvert(positions, level, parent, root) {
           }
 
           // add col meta
+          // colContainer.meta.width = getChildrenMax(colContainer.children, 'width', root);
+          // colContainer.meta.height = getChildrenSum(colContainer.children, 'height', root);
           colContainer.meta.width = getChildrenMax(colContainer.children, 'width', root);
-          colContainer.meta.height = getChildrenSum(colContainer.children, 'height', root);
 
           currentItems = upper.slice();
         }
         currentCol++;
       }
     }
-
-    rowContainer.meta.width = getChildrenSum(rowContainer.children, 'width', root);
-    rowContainer.meta.height = getChildrenMax(rowContainer.children, 'height', root);
   });
 }
 
@@ -305,4 +313,3 @@ export default function(dashboard) {
   // console.log(JSON.stringify(root));
   return root;
 }
-
diff --git a/superset/assets/javascripts/dashboard/v2/actions/dashboardLayout.js b/superset/assets/javascripts/dashboard/v2/actions/dashboardLayout.js
index 4958710..4cc795b 100644
--- a/superset/assets/javascripts/dashboard/v2/actions/dashboardLayout.js
+++ b/superset/assets/javascripts/dashboard/v2/actions/dashboardLayout.js
@@ -1,6 +1,6 @@
 import { addInfoToast } from './messageToasts';
 import { CHART_TYPE, MARKDOWN_TYPE, TABS_TYPE } from '../util/componentTypes';
-import { DASHBOARD_ROOT_ID, NEW_COMPONENTS_SOURCE_ID } from '../util/constants';
+import { DASHBOARD_ROOT_ID, NEW_COMPONENTS_SOURCE_ID, GRID_MIN_COLUMN_COUNT } from '../util/constants';
 import dropOverflowsParent from '../util/dropOverflowsParent';
 import findParentId from '../util/findParentId';
 
@@ -62,12 +62,10 @@ export function resizeComponent({ id, width, height }) {
     const { dashboardLayout: undoableLayout } = getState();
     const { present: dashboard } = undoableLayout;
     const component = dashboard[id];
-
-    if (
-      component &&
-      (component.meta.width !== width || component.meta.height !== height)
-    ) {
-      // update the size of this component + any resizable children
+    const widthChanged = width && component.meta.width !== width;
+    const heightChanged = height && component.meta.height !== height;
+    if (component && (widthChanged || heightChanged)) {
+      // update the size of this component
       const updatedComponents = {
         [id]: {
           ...component,
@@ -79,6 +77,8 @@ export function resizeComponent({ id, width, height }) {
         },
       };
 
+      // set any resizable children to have a minimum width so that
+      // the chances that they are validly movable to future containers is maximized
       component.children.forEach((childId) => {
         const child = dashboard[childId];
         if ([CHART_TYPE, MARKDOWN_TYPE].includes(child.type)) {
@@ -86,14 +86,16 @@ export function resizeComponent({ id, width, height }) {
             ...child,
             meta: {
               ...child.meta,
-              width: width || child.meta.width,
+              width: GRID_MIN_COLUMN_COUNT,
               height: height || child.meta.height,
             },
           };
         }
       });
 
-      dispatch(updateComponents(updatedComponents));
+      dispatch(
+        updateComponents(updatedComponents),
+      );
     }
   };
 }
diff --git a/superset/assets/javascripts/dashboard/v2/components/BuilderComponentPane.jsx b/superset/assets/javascripts/dashboard/v2/components/BuilderComponentPane.jsx
index e9e2327..b09834b 100644
--- a/superset/assets/javascripts/dashboard/v2/components/BuilderComponentPane.jsx
+++ b/superset/assets/javascripts/dashboard/v2/components/BuilderComponentPane.jsx
@@ -37,8 +37,7 @@ class BuilderComponentPane extends React.PureComponent {
         <div className="dashboard-builder-sidepane-header">
           Insert components
           {this.state.showSlices &&
-            <i className="fa fa-times close trigger" onClick={this.closeSlicesPane}/>
-          }
+            <i className="fa fa-times close trigger" onClick={this.closeSlicesPane} />}
         </div>
 
         <div className="component-layer">
@@ -51,9 +50,7 @@ class BuilderComponentPane extends React.PureComponent {
           </div>
 
           <NewHeader />
-        <NewDivider />
-
-
+          <NewDivider />
           <NewTabs />
           <NewRow />
           <NewColumn />
diff --git a/superset/assets/javascripts/dashboard/v2/components/DashboardBuilder.jsx b/superset/assets/javascripts/dashboard/v2/components/DashboardBuilder.jsx
index f3f5867..28ea951 100644
--- a/superset/assets/javascripts/dashboard/v2/components/DashboardBuilder.jsx
+++ b/superset/assets/javascripts/dashboard/v2/components/DashboardBuilder.jsx
@@ -20,8 +20,6 @@ import {
 } from '../util/constants';
 
 const propTypes = {
-  cells: PropTypes.object.isRequired,
-
   // redux
   dashboardLayout: PropTypes.object.isRequired,
   deleteTopLevelTabs: PropTypes.func.isRequired,
@@ -108,7 +106,6 @@ class DashboardBuilder extends React.Component {
               index={0}
               renderTabContent={false}
               onChangeTab={this.handleChangeTab}
-              cells={this.props.cells}
             />
           </WithPopoverMenu>}
 
@@ -116,7 +113,6 @@ class DashboardBuilder extends React.Component {
           <DashboardGrid
             gridComponent={gridComponent}
             depth={DASHBOARD_ROOT_DEPTH + 1}
-            cells={this.props.cells}
           />
           {this.props.editMode && this.props.showBuilderPane &&
             <BuilderComponentPane />
diff --git a/superset/assets/javascripts/dashboard/v2/components/DashboardGrid.jsx b/superset/assets/javascripts/dashboard/v2/components/DashboardGrid.jsx
index 2aa82af..4cc73b9 100644
--- a/superset/assets/javascripts/dashboard/v2/components/DashboardGrid.jsx
+++ b/superset/assets/javascripts/dashboard/v2/components/DashboardGrid.jsx
@@ -1,5 +1,7 @@
 import React from 'react';
 import PropTypes from 'prop-types';
+// ParentSize uses resize observer so the dashboard will update size
+// when its container size changes, due to e.g., builder side panel opening
 import ParentSize from '@vx/responsive/build/components/ParentSize';
 
 import { componentShape } from '../util/propShapes';
@@ -34,6 +36,7 @@ class DashboardGrid extends React.PureComponent {
     this.handleResize = this.handleResize.bind(this);
     this.handleResizeStop = this.handleResizeStop.bind(this);
     this.getRowGuidePosition = this.getRowGuidePosition.bind(this);
+    this.setGridRef = this.setGridRef.bind(this);
   }
 
   getRowGuidePosition(resizeRef) {
@@ -43,6 +46,10 @@ class DashboardGrid extends React.PureComponent {
     return null;
   }
 
+  setGridRef(ref) {
+    this.grid = ref;
+  }
+
   handleResizeStart({ ref, direction }) {
     let rowGuideTop = null;
     if (direction === 'bottom' || direction === 'bottomRight') {
@@ -71,73 +78,72 @@ class DashboardGrid extends React.PureComponent {
   }
 
   render() {
-    const { gridComponent, handleComponentDrop, depth, editMode, cells } = this.props;
+    const { gridComponent, handleComponentDrop, depth, editMode } = this.props;
     const { isResizing, rowGuideTop } = this.state;
 
     return (
-      <div className="grid-container" ref={(ref) => { this.grid = ref; }}>
+      <div className="grid-container" ref={this.setGridRef}>
         <ParentSize>
-          {({ width }) => {
-            // account for (COLUMN_COUNT - 1) gutters
+          {(({ width }) => {
             const columnPlusGutterWidth = (width + GRID_GUTTER_SIZE) / GRID_COLUMN_COUNT;
             const columnWidth = columnPlusGutterWidth - GRID_GUTTER_SIZE;
-
-            return width < 50 ? null : (
-              <div className="grid-content">
-                {gridComponent.children.map((id, index) => (
-                  <DashboardComponent
-                    key={id}
-                    id={id}
-                    parentId={gridComponent.id}
-                    depth={depth + 1}
-                    index={index}
-                    availableColumnCount={GRID_COLUMN_COUNT}
-                    columnWidth={columnWidth}
-                    cells={cells}
-                    onResizeStart={this.handleResizeStart}
-                    onResize={this.handleResize}
-                    onResizeStop={this.handleResizeStop}
-                  />
-                ))}
-
-                {/* render an empty drop target */}
-                {editMode &&
-                  <DragDroppable
-                    component={gridComponent}
-                    depth={depth}
-                    parentComponent={null}
-                    index={gridComponent.children.length}
-                    orientation="column"
-                    onDrop={handleComponentDrop}
-                    className="empty-grid-droptarget"
-                    editMode
-                  >
-                    {({ dropIndicatorProps }) => dropIndicatorProps &&
-                      <div className="drop-indicator drop-indicator--top" />}
-                  </DragDroppable>}
-
-                {isResizing && Array(GRID_COLUMN_COUNT).fill(null).map((_, i) => (
-                  <div
-                    key={`grid-column-${i}`}
-                    className="grid-column-guide"
-                    style={{
-                      left: (i * GRID_GUTTER_SIZE) + (i * columnWidth),
-                      width: columnWidth,
-                    }}
-                  />
-                ))}
-
-                {isResizing && rowGuideTop &&
-                  <div
-                    className="grid-row-guide"
-                    style={{
-                      top: rowGuideTop,
-                      width,
-                    }}
-                  />}
-              </div>
+            return (
+              width < 50 ? null : (
+                <div className="grid-content">
+                  {gridComponent.children.map((id, index) => (
+                    <DashboardComponent
+                      key={id}
+                      id={id}
+                      parentId={gridComponent.id}
+                      depth={depth + 1}
+                      index={index}
+                      availableColumnCount={GRID_COLUMN_COUNT}
+                      columnWidth={columnWidth}
+                      onResizeStart={this.handleResizeStart}
+                      onResize={this.handleResize}
+                      onResizeStop={this.handleResizeStop}
+                    />
+                  ))}
+
+                  {/* render an empty drop target */}
+                  {editMode &&
+                    <DragDroppable
+                      component={gridComponent}
+                      depth={depth}
+                      parentComponent={null}
+                      index={gridComponent.children.length}
+                      orientation="column"
+                      onDrop={handleComponentDrop}
+                      className="empty-grid-droptarget"
+                      editMode
+                    >
+                      {({ dropIndicatorProps }) => dropIndicatorProps &&
+                        <div className="drop-indicator drop-indicator--top" />}
+                    </DragDroppable>}
+
+                  {isResizing && Array(GRID_COLUMN_COUNT).fill(null).map((_, i) => (
+                    <div
+                      key={`grid-column-${i}`}
+                      className="grid-column-guide"
+                      style={{
+                        left: (i * GRID_GUTTER_SIZE) + (i * columnWidth),
+                        width: columnWidth,
+                      }}
+                    />
+                  ))}
+
+                  {isResizing && rowGuideTop &&
+                    <div
+                      className="grid-row-guide"
+                      style={{
+                        top: rowGuideTop,
+                        width,
+                      }}
+                    />}
+                </div>
+              )
             );
-          }}
+          })}
         </ParentSize>
       </div>
     );
diff --git a/superset/assets/javascripts/dashboard/v2/components/WithKeyListener.jsx b/superset/assets/javascripts/dashboard/v2/components/WithKeyListener.jsx
new file mode 100644
index 0000000..b391387
--- /dev/null
+++ b/superset/assets/javascripts/dashboard/v2/components/WithKeyListener.jsx
@@ -0,0 +1,55 @@
+import React from 'react';
+import PropTypes from 'prop-types';
+
+const TEST_CACHE = {
+  cmdZ: e => (e.metaKey || e.ctrlKey) && e.keyCode === 90,
+};
+
+const propTypes = {
+  // accepts keyCode
+  // or a test func (which is lazily cached using the cacheKey string)
+  keyCode: PropTypes.number,
+  cacheKey: PropTypes.string,
+  test: PropTypes.func, // (event) => Boolean
+  onPress: PropTypes.func.isRequired,
+};
+
+export default class WithKeyListener extends React.PureComponent {
+  componentDidMount() {
+    const { keyCode, test, cacheKey, onPress } = this.props;
+    let eventListener;
+    if (test && cacheKey) { // overwrite cache
+      TEST_CACHE[cacheKey] = test;
+      eventListener = test;
+    } else if (cacheKey && TEST_CACHE[cacheKey]) { // use cache
+      eventListener = TEST_CACHE[cacheKey]
+    } else if (typeof keyCode === 'number') {
+      if (TEST_CACHE[keyCode]) {
+        eventListener = TEST_CACHE[cacheKey]; // use keyCode cache
+      } else {
+        TEST_CACHE[cacheKey] = e => e.keyCode === keyCode; // set cache
+        eventListener = TEST_CACHE[cacheKey];
+      }
+    } else {
+      console.warn('Missing cacheKey, test, or keyCode');
+      return;
+    }
+
+    document.addEventListener('keydown', (e) => {
+      if (eventListener(e)) {
+        onPress(e);
+        alert('keydown');
+      }
+    });
+  }
+
+  componentWillUnmount() {
+    document.removeEventListener('keydown');
+  }
+
+  render() {
+    return null;
+  }
+}
+
+WithKeyListener.propTypes = propTypes;
diff --git a/superset/assets/javascripts/dashboard/v2/components/dnd/DragDroppable.jsx b/superset/assets/javascripts/dashboard/v2/components/dnd/DragDroppable.jsx
index 775e092..6e2838a 100644
--- a/superset/assets/javascripts/dashboard/v2/components/dnd/DragDroppable.jsx
+++ b/superset/assets/javascripts/dashboard/v2/components/dnd/DragDroppable.jsx
@@ -74,7 +74,7 @@ class DragDroppable extends React.Component {
       editMode,
     } = this.props;
 
-    if (!editMode) return children({});
+    // if (!editMode) return children({});
 
     const { dropIndicator } = this.state;
 
@@ -90,7 +90,7 @@ class DragDroppable extends React.Component {
           className,
         )}
       >
-        {children({
+        {children(!editMode ? {} : {
           dragSourceRef,
           dropIndicatorProps: isDraggingOver && dropIndicator && {
             className: cx(
diff --git a/superset/assets/javascripts/dashboard/v2/components/gridComponents/Chart.jsx b/superset/assets/javascripts/dashboard/v2/components/gridComponents/Chart.jsx
new file mode 100644
index 0000000..889a455
--- /dev/null
+++ b/superset/assets/javascripts/dashboard/v2/components/gridComponents/Chart.jsx
@@ -0,0 +1,206 @@
+import React from 'react';
+import PropTypes from 'prop-types';
+
+import { exportChart } from '../../../../explore/exploreUtils';
+import SliceHeader from '../../../components/SliceHeader';
+import ChartContainer from '../../../../chart/ChartContainer';
+import { chartPropType } from '../../../../chart/chartReducer';
+import { slicePropShape } from '../../../reducers/propShapes';
+
+const propTypes = {
+  id: PropTypes.string.isRequired,
+  width: PropTypes.number.isRequired,
+  height: PropTypes.number.isRequired,
+
+  // from redux
+  chart: PropTypes.shape(chartPropType).isRequired,
+  formData: PropTypes.object.isRequired,
+  datasource: PropTypes.object.isRequired,
+  slice: slicePropShape.isRequired,
+  timeout: PropTypes.number.isRequired,
+  filters: PropTypes.object.isRequired,
+  refreshChart: PropTypes.func.isRequired,
+  saveSliceName: PropTypes.func.isRequired,
+  toggleExpandSlice: PropTypes.func.isRequired,
+  addFilter: PropTypes.func.isRequired,
+  removeFilter: PropTypes.func.isRequired,
+  editMode: PropTypes.bool.isRequired,
+  isExpanded: PropTypes.bool.isRequired,
+};
+
+const updateOnPropChange = Object.keys(propTypes)
+  .filter(prop => prop !== 'width' && prop !== 'height');
+
+class Chart extends React.Component {
+  constructor(props) {
+    super(props);
+    this.state = {
+      width: props.width,
+      height: props.height,
+    };
+
+    this.addFilter = this.addFilter.bind(this);
+    this.exploreChart = this.exploreChart.bind(this);
+    this.exportCSV = this.exportCSV.bind(this);
+    this.forceRefresh = this.forceRefresh.bind(this);
+    this.getFilters = this.getFilters.bind(this);
+    this.removeFilter = this.removeFilter.bind(this);
+    this.resize = this.resize.bind(this);
+    this.setDescriptionRef = this.setDescriptionRef.bind(this);
+    this.setHeaderRef = this.setHeaderRef.bind(this);
+  }
+
+  shouldComponentUpdate(nextProps, nextState) {
+    if (nextState.width !== this.state.width || nextState.height !== this.state.height) {
+      return true;
+    }
+
+    for (let i = 0; i < updateOnPropChange.length; i += 1) {
+      const prop = updateOnPropChange[i];
+      if (nextProps[prop] !== this.props[prop]) {
+        console.log(prop, 'changed')
+        return true;
+      }
+    }
+
+    if (nextProps.width !== this.props.width || nextProps.height !== this.props.height) {
+      clearTimeout(this.resizeTimeout);
+      this.resizeTimeout = setTimeout(this.resize, 350);
+    }
+
+    return false;
+  }
+
+  componentWillUnmount() {
+    clearTimeout(this.resizeTimeout);
+  }
+
+  getFilters() {
+    return this.props.filters;
+  }
+
+  getChartHeight() {
+    const headerHeight = this.getHeaderHeight();
+    const descriptionHeight = this.props.isExpanded && this.descriptionRef
+      ? this.descriptionRef.offsetHeight : 0;
+
+    return this.state.height - headerHeight - descriptionHeight;
+  }
+
+  getHeaderHeight() {
+    return (this.headerRef && this.headerRef.offsetHeight) || 30;
+  }
+
+  setDescriptionRef(ref) {
+    this.descriptionRef = ref;
+  }
+
+  setHeaderRef(ref) {
+    this.headerRef = ref;
+  }
+
+  resize() {
+    const { width, height } = this.props;
+    this.setState(() => ({ width, height }));
+  }
+
+  addFilter(args) {
+    this.props.addFilter(this.props.chart, ...args);
+  }
+
+  exploreChart() {
+    exportChart(this.props.formData);
+  }
+
+  exportCSV() {
+    exportChart(this.props.formData, 'csv');
+  }
+
+  forceRefresh() {
+    return this.props.refreshChart(this.props.chart, true, this.props.timeout);
+  }
+
+  removeFilter(args) {
+    this.props.removeFilter(this.props.id, ...args);
+  }
+
+  render() {
+    const {
+      id,
+      chart,
+      slice,
+      datasource,
+      isExpanded,
+      editMode,
+      formData,
+      toggleExpandSlice,
+      timeout,
+    } = this.props;
+
+    const { width } = this.state;
+    const { queryResponse } = chart;
+    const isCached = queryResponse && queryResponse.is_cached;
+    const cachedDttm = queryResponse && queryResponse.cached_dttm;
+
+    return (
+      <div className="dashboard-chart">
+        <SliceHeader
+          innerRef={this.setHeaderRef}
+          slice={slice}
+          isExpanded={!!isExpanded}
+          isCached={isCached}
+          cachedDttm={cachedDttm}
+          updateSliceName={this.updateSliceName}
+          toggleExpandSlice={toggleExpandSlice}
+          forceRefresh={this.forceRefresh}
+          editMode={editMode}
+          annotationQuery={chart.annotationQuery}
+          exploreChart={this.exploreChart}
+          exportCSV={this.exportCSV}
+        />
+        {/*
+          This usage of dangerouslySetInnerHTML is safe since it is being used to render
+          markdown that is sanitized with bleach. See:
+             https://github.com/apache/incubator-superset/pull/4390
+          and
+             https://github.com/apache/incubator-superset/commit/b6fcc22d5a2cb7a5e92599ed5795a0169385a825
+        */}
+        <div
+          className="slice_description bs-callout bs-callout-default"
+          style={isExpanded ? null : { display: 'none' }}
+          ref={this.setDescriptionRef}
+          // eslint-disable-next-line react/no-danger
+          dangerouslySetInnerHTML={{ __html: slice.description_markeddown }}
+        />
+        <ChartContainer
+          containerId={`slice-container-${slice.slice_id}`}
+          sliceId={id}
+          datasource={datasource}
+          formData={formData}
+          headerHeight={this.getHeaderHeight()}
+          height={this.getChartHeight()}
+          width={width}
+          timeout={timeout}
+          vizType={slice.viz_type}
+          addFilter={this.addFilter}
+          getFilters={this.getFilters}
+          removeFilter={this.removeFilter}
+          annotationData={chart.annotationData}
+          chartAlert={chart.chartAlert}
+          chartStatus={chart.chartStatus}
+          chartUpdateEndTime={chart.chartUpdateEndTime}
+          chartUpdateStartTime={chart.chartUpdateStartTime}
+          latestQueryFormData={chart.latestQueryFormData}
+          lastRendered={chart.lastRendered}
+          queryResponse={chart.queryResponse}
+          queryRequest={chart.queryRequest}
+          triggerQuery={chart.triggerQuery}
+        />
+      </div>
+    );
+  }
+}
+
+Chart.propTypes = propTypes;
+
+export default Chart;
diff --git a/superset/assets/javascripts/dashboard/v2/components/gridComponents/ChartHolder.jsx b/superset/assets/javascripts/dashboard/v2/components/gridComponents/ChartHolder.jsx
index ae304ad..28ddad1 100644
--- a/superset/assets/javascripts/dashboard/v2/components/gridComponents/ChartHolder.jsx
+++ b/superset/assets/javascripts/dashboard/v2/components/gridComponents/ChartHolder.jsx
@@ -1,15 +1,19 @@
 import React from 'react';
 import PropTypes from 'prop-types';
 
+import Chart from '../../containers/Chart';
 import DeleteComponentButton from '../DeleteComponentButton';
 import DragDroppable from '../dnd/DragDroppable';
 import DragHandle from '../dnd/DragHandle';
 import HoverMenu from '../menu/HoverMenu';
 import ResizableContainer from '../resizable/ResizableContainer';
-import WithPopoverMenu from '../menu/WithPopoverMenu';
 import { componentShape } from '../../util/propShapes';
-import { ROW_TYPE } from '../../util/componentTypes';
-import { GRID_MIN_COLUMN_COUNT, GRID_MIN_ROW_UNITS } from '../../util/constants';
+import { ROW_TYPE, COLUMN_TYPE } from '../../util/componentTypes';
+import {
+  GRID_MIN_COLUMN_COUNT,
+  GRID_MIN_ROW_UNITS,
+  GRID_BASE_UNIT,
+} from '../../util/constants';
 
 const propTypes = {
   id: PropTypes.string.isRequired,
@@ -19,7 +23,6 @@ const propTypes = {
   index: PropTypes.number.isRequired,
   depth: PropTypes.number.isRequired,
   editMode: PropTypes.bool.isRequired,
-  chart: PropTypes.object,
 
   // grid related
   availableColumnCount: PropTypes.number.isRequired,
@@ -73,6 +76,11 @@ class ChartHolder extends React.Component {
       editMode,
     } = this.props;
 
+    // inherit the size of parent columns
+    const widthMultiple = parentComponent.type === COLUMN_TYPE
+      ? parentComponent.meta.width || GRID_MIN_COLUMN_COUNT
+      : component.meta.width || GRID_MIN_COLUMN_COUNT;
+
     return (
       <DragDroppable
         component={component}
@@ -90,34 +98,31 @@ class ChartHolder extends React.Component {
             adjustableWidth={parentComponent.type === ROW_TYPE}
             adjustableHeight
             widthStep={columnWidth}
-            widthMultiple={component.meta.width}
+            widthMultiple={widthMultiple}
+            heightStep={GRID_BASE_UNIT}
             heightMultiple={component.meta.height}
             minWidthMultiple={GRID_MIN_COLUMN_COUNT}
             minHeightMultiple={GRID_MIN_ROW_UNITS}
-            maxWidthMultiple={availableColumnCount + (component.meta.width || 0)}
+            maxWidthMultiple={availableColumnCount + widthMultiple}
             onResizeStart={onResizeStart}
             onResize={onResize}
             onResizeStop={onResizeStop}
             editMode={editMode}
           >
-            {editMode &&
-              <HoverMenu innerRef={dragSourceRef} position="top">
-                <DragHandle position="top" />
-              </HoverMenu>}
-
-            <WithPopoverMenu
-              onChangeFocus={this.handleChangeFocus}
-              menuItems={[
-                <DeleteComponentButton onDelete={this.handleDeleteComponent} />,
-              ]}
-              editMode={editMode}
-            >
-              <div className="dashboard-component dashboard-component-chart">
-                {this.props.chart}
-              </div>
-
-              {dropIndicatorProps && <div {...dropIndicatorProps} />}
-            </WithPopoverMenu>
+            <div ref={dragSourceRef} className="dashboard-component dashboard-component-chart">
+              <Chart
+                id={component.meta.chartKey}
+                width={widthMultiple * columnWidth}
+                height={component.meta.height * GRID_BASE_UNIT}
+              />
+              {editMode &&
+                <HoverMenu position="top">
+                  <DragHandle position="top" />
+                  <DeleteComponentButton onDelete={this.handleDeleteComponent} />
+                </HoverMenu>}
+            </div>
+
+            {dropIndicatorProps && <div {...dropIndicatorProps} />}
           </ResizableContainer>
         )}
       </DragDroppable>
diff --git a/superset/assets/javascripts/dashboard/v2/components/gridComponents/Column.jsx b/superset/assets/javascripts/dashboard/v2/components/gridComponents/Column.jsx
index 634c1a4..03e0ab4 100644
--- a/superset/assets/javascripts/dashboard/v2/components/gridComponents/Column.jsx
+++ b/superset/assets/javascripts/dashboard/v2/components/gridComponents/Column.jsx
@@ -25,7 +25,6 @@ const propTypes = {
   index: PropTypes.number.isRequired,
   depth: PropTypes.number.isRequired,
   editMode: PropTypes.bool.isRequired,
-  cells: PropTypes.object.isRequired,
 
   // grid related
   availableColumnCount: PropTypes.number.isRequired,
@@ -93,7 +92,6 @@ class Column extends React.PureComponent {
       onResizeStop,
       handleComponentDrop,
       editMode,
-      cells,
     } = this.props;
 
     const columnItems = columnComponent.children || [];
@@ -156,20 +154,19 @@ class Column extends React.PureComponent {
                   </HoverMenu>}
 
                 {columnItems.map((componentId, itemIndex) => (
-
-                    <DashboardComponent
-                      key={componentId}
-                      id={componentId}
-                      parentId={columnComponent.id}
-                      depth={depth + 1}
-                      index={itemIndex }
-                      availableColumnCount={columnComponent.meta.width}
-                      columnWidth={columnWidth}
-                      cells={cells}onResizeStart={onResizeStart}
-                      onResize={onResize}
-                      onResizeStop={onResizeStop}
-                    />
-                  ))}
+                  <DashboardComponent
+                    key={componentId}
+                    id={componentId}
+                    parentId={columnComponent.id}
+                    depth={depth + 1}
+                    index={itemIndex }
+                    availableColumnCount={columnComponent.meta.width}
+                    columnWidth={columnWidth}
+                    onResizeStart={onResizeStart}
+                    onResize={onResize}
+                    onResizeStop={onResizeStop}
+                  />
+                ))}
 
                 {dropIndicatorProps && <div {...dropIndicatorProps} />}
               </div>
diff --git a/superset/assets/javascripts/dashboard/v2/components/gridComponents/Row.jsx b/superset/assets/javascripts/dashboard/v2/components/gridComponents/Row.jsx
index 8faaee1..9866bc8 100644
--- a/superset/assets/javascripts/dashboard/v2/components/gridComponents/Row.jsx
+++ b/superset/assets/javascripts/dashboard/v2/components/gridComponents/Row.jsx
@@ -23,7 +23,6 @@ const propTypes = {
   index: PropTypes.number.isRequired,
   depth: PropTypes.number.isRequired,
   editMode: PropTypes.bool.isRequired,
-  cells: PropTypes.object.isRequired,
 
   // grid related
   availableColumnCount: PropTypes.number.isRequired,
@@ -93,7 +92,6 @@ class Row extends React.PureComponent {
       onResizeStop,
       handleComponentDrop,
       editMode,
-      cells,
     } = this.props;
 
     const rowItems = rowComponent.children || [];
@@ -144,20 +142,19 @@ class Row extends React.PureComponent {
                 </HoverMenu>}
 
               {rowItems.map((componentId, itemIndex) => (
-
-                  <DashboardComponent
-                    key={componentId}
-                    id={componentId}
-                    parentId={rowComponent.id}
-                    depth={depth + 1}
-                    index={itemIndex }
-                    availableColumnCount={availableColumnCount - occupiedColumnCount}
-                    columnWidth={columnWidth}
-                    cells={cells}onResizeStart={onResizeStart}
-                    onResize={onResize}
-                    onResizeStop={onResizeStop}
-                  />
-                ))}
+                <DashboardComponent
+                  key={componentId}
+                  id={componentId}
+                  parentId={rowComponent.id}
+                  depth={depth + 1}
+                  index={itemIndex}
+                  availableColumnCount={availableColumnCount - occupiedColumnCount}
+                  columnWidth={columnWidth}
+                  onResizeStart={onResizeStart}
+                  onResize={onResize}
+                  onResizeStop={onResizeStop}
+                />
+              ))}
 
               {dropIndicatorProps && <div {...dropIndicatorProps} />}
             </div>
diff --git a/superset/assets/javascripts/dashboard/v2/components/menu/WithPopoverMenu.jsx b/superset/assets/javascripts/dashboard/v2/components/menu/WithPopoverMenu.jsx
index f213442..1e93181 100644
--- a/superset/assets/javascripts/dashboard/v2/components/menu/WithPopoverMenu.jsx
+++ b/superset/assets/javascripts/dashboard/v2/components/menu/WithPopoverMenu.jsx
@@ -54,12 +54,11 @@ class WithPopoverMenu extends React.PureComponent {
   }
 
   handleClick(event) {
-    const { onChangeFocus, shouldFocus: shouldFocusFunc, disableClick, editMode } = this.props;
-    const shouldFocus = shouldFocusFunc(event, this.container);
-
-    if (!editMode) {
+    if (!this.props.editMode) {
       return;
     }
+    const { onChangeFocus, shouldFocus: shouldFocusFunc, disableClick } = this.props;
+    const shouldFocus = shouldFocusFunc(event, this.container);
 
     if (!disableClick && shouldFocus && !this.state.isFocused) {
       // if not focused, set focus and add a window event listener to capture outside clicks
diff --git a/superset/assets/javascripts/dashboard/v2/components/resizable/ResizableContainer.jsx b/superset/assets/javascripts/dashboard/v2/components/resizable/ResizableContainer.jsx
index a532ff0..2bb6c08 100644
--- a/superset/assets/javascripts/dashboard/v2/components/resizable/ResizableContainer.jsx
+++ b/superset/assets/javascripts/dashboard/v2/components/resizable/ResizableContainer.jsx
@@ -56,7 +56,10 @@ const defaultProps = {
 // because columns are not multiples of a single variable (width = n*cols + (n-1) * gutters)
 // we snap to the base unit and then snap to _actual_ column multiples on stop
 const SNAP_TO_GRID = [GRID_BASE_UNIT, GRID_BASE_UNIT];
-
+const HANDLE_CLASSES = {
+  right: 'resizable-container-handle--right',
+  bottom: 'resizable-container-handle--bottom',
+};
 class ResizableContainer extends React.PureComponent {
   constructor(props) {
     super(props);
@@ -150,18 +153,15 @@ class ResizableContainer extends React.PureComponent {
           || undefined,
     };
 
-    if (!editMode) {
-      return (
-        <div style={{ ...size }}>
-          {children}
-        </div>
-      );
-    }
-
     let enableConfig = resizableConfig.notAdjustable;
-    if (adjustableWidth && adjustableHeight) enableConfig = resizableConfig.widthAndHeight;
-    else if (adjustableWidth) enableConfig = resizableConfig.widthOnly;
-    else if (adjustableHeight) enableConfig = resizableConfig.heightOnly;
+
+    if (editMode && adjustableWidth && adjustableHeight) {
+      enableConfig = resizableConfig.widthAndHeight;
+    } else if (editMode && adjustableWidth) {
+      enableConfig = resizableConfig.widthOnly;
+    } else if (editMode && adjustableHeight) {
+      enableConfig = resizableConfig.heightOnly;
+    }
 
     const { isResizing } = this.state;
 
@@ -190,6 +190,7 @@ class ResizableContainer extends React.PureComponent {
           'resizable-container',
           isResizing && 'resizable-container--resizing',
         )}
+        handleClasses={HANDLE_CLASSES}
       >
         {children}
       </Resizable>
diff --git a/superset/assets/javascripts/dashboard/v2/containers/Chart.jsx b/superset/assets/javascripts/dashboard/v2/containers/Chart.jsx
new file mode 100644
index 0000000..d527c9b
--- /dev/null
+++ b/superset/assets/javascripts/dashboard/v2/containers/Chart.jsx
@@ -0,0 +1,44 @@
+import { bindActionCreators } from 'redux';
+import { connect } from 'react-redux';
+
+import { addFilter, removeFilter, toggleExpandSlice } from '../../actions/dashboard';
+import { refreshChart } from '../../../chart/chartAction';
+import getFormDataWithExtraFilters from '../../v2/util/charts/getFormDataWithExtraFilters';
+import { saveSliceName } from '../../actions/allSlices';
+import Chart from '../components/gridComponents/Chart';
+
+function mapStateToProps({ datasources, allSlices, charts, dashboard }, ownProps) {
+  const { id } = ownProps;
+  const chart = charts[id];
+  const { filters } = dashboard;
+  const isExpanded = !!(dashboard.dashboard.metadata.expanded_slices || {})[id];
+
+  return {
+    chart,
+    datasource: datasources[chart.form_data.datasource],
+    slice: allSlices.slices[id],
+    timeout: dashboard.common.conf.SUPERSET_WEBSERVER_TIMEOUT,
+    filters,
+    // note: this method caches filters if possible to prevent render cascades
+    formData: getFormDataWithExtraFilters({
+      chart,
+      dashboardMetadata: dashboard.dashboard.metadata,
+      filters,
+      sliceId: id,
+    }),
+    editMode: dashboard.editMode,
+    isExpanded,
+  };
+}
+
+function mapDispatchToProps(dispatch) {
+  return bindActionCreators({
+    saveSliceName,
+    toggleExpandSlice,
+    addFilter,
+    refreshChart,
+    removeFilter,
+  }, dispatch);
+}
+
+export default connect(mapStateToProps, mapDispatchToProps)(Chart);
diff --git a/superset/assets/javascripts/dashboard/v2/containers/DashboardBuilder.jsx b/superset/assets/javascripts/dashboard/v2/containers/DashboardBuilder.jsx
index ba9fc45..a70126c 100644
--- a/superset/assets/javascripts/dashboard/v2/containers/DashboardBuilder.jsx
+++ b/superset/assets/javascripts/dashboard/v2/containers/DashboardBuilder.jsx
@@ -7,10 +7,9 @@ import {
   handleComponentDrop,
 } from '../actions/dashboardLayout';
 
-function mapStateToProps({ dashboardLayout: undoableLayout, dashboard }, ownProps) {
+function mapStateToProps({ dashboardLayout: undoableLayout, dashboard }) {
   return {
     dashboardLayout: undoableLayout.present,
-    cells: ownProps.cells,
     editMode: dashboard.editMode,
     showBuilderPane: dashboard.showBuilderPane,
   };
diff --git a/superset/assets/javascripts/dashboard/v2/containers/DashboardComponent.jsx b/superset/assets/javascripts/dashboard/v2/containers/DashboardComponent.jsx
index 3118ce8..3baa84b 100644
--- a/superset/assets/javascripts/dashboard/v2/containers/DashboardComponent.jsx
+++ b/superset/assets/javascripts/dashboard/v2/containers/DashboardComponent.jsx
@@ -6,7 +6,7 @@ import { connect } from 'react-redux';
 import ComponentLookup from '../components/gridComponents';
 import getTotalChildWidth from '../util/getChildWidth';
 import { componentShape } from '../util/propShapes';
-import { CHART_TYPE, COLUMN_TYPE, ROW_TYPE } from '../util/componentTypes';
+import { COLUMN_TYPE, ROW_TYPE } from '../util/componentTypes';
 import { GRID_MIN_COLUMN_COUNT } from '../util/constants';
 
 import {
@@ -25,9 +25,15 @@ const propTypes = {
   handleComponentDrop: PropTypes.func.isRequired,
 };
 
-function mapStateToProps({ dashboardLayout: undoableLayout, dashboard }, ownProps) {
+function mapStateToProps({
+  dashboardLayout: undoableLayout,
+  dashboard,
+  allSlices,
+  charts,
+  datasources,
+}, ownProps) {
   const dashboardLayout = undoableLayout.present;
-  const { id, parentId, cells } = ownProps;
+  const { id, parentId } = ownProps;
   const component = dashboardLayout[id];
   const props = {
     component,
@@ -51,11 +57,6 @@ function mapStateToProps({ dashboardLayout: undoableLayout, dashboard }, ownProp
         );
       }
     });
-  } else if (props.component.type === CHART_TYPE) {
-    const chartKey = props.component.meta && props.component.meta.chartKey;
-    if (chartKey) {
-      props.chart = cells[chartKey];
-    }
   }
 
   return props;
diff --git a/superset/assets/javascripts/dashboard/v2/containers/DashboardGrid.jsx b/superset/assets/javascripts/dashboard/v2/containers/DashboardGrid.jsx
index 9aa3447..ef2eb8c 100644
--- a/superset/assets/javascripts/dashboard/v2/containers/DashboardGrid.jsx
+++ b/superset/assets/javascripts/dashboard/v2/containers/DashboardGrid.jsx
@@ -7,10 +7,9 @@ import {
   resizeComponent,
 } from '../actions/dashboardLayout';
 
-function mapStateToProps({ dashboard }, ownProps) {
+function mapStateToProps({ dashboard }) {
   return {
     editMode: dashboard.editMode,
-    cells: ownProps.cells,
   };
 }
 
diff --git a/superset/assets/javascripts/dashboard/v2/reducers/dashboardLayout.js b/superset/assets/javascripts/dashboard/v2/reducers/dashboardLayout.js
index 994ac47..421463a 100644
--- a/superset/assets/javascripts/dashboard/v2/reducers/dashboardLayout.js
+++ b/superset/assets/javascripts/dashboard/v2/reducers/dashboardLayout.js
@@ -1,4 +1,8 @@
-import { DASHBOARD_ROOT_ID, DASHBOARD_GRID_ID, NEW_COMPONENTS_SOURCE_ID } from '../util/constants';
+import {
+  DASHBOARD_ROOT_ID,
+  DASHBOARD_GRID_ID,
+  GRID_MIN_COLUMN_COUNT,
+  NEW_COMPONENTS_SOURCE_ID } from '../util/constants';
 import newComponentFactory from '../util/newComponentFactory';
 import newEntitiesFromDrop from '../util/newEntitiesFromDrop';
 import reorderItem from '../util/dnd-reorder';
@@ -70,17 +74,17 @@ const actionHandlers = {
     const { destination, dragging } = dropResult;
     const newEntities = newEntitiesFromDrop({ dropResult, components: state });
 
-    // inherit the width of a column parent
+    // if column is a parent, set any resizable children to have a minimum width so that
+    // the chances that they are validly movable to future containers is maximized
     if (destination.type === COLUMN_TYPE && [CHART_TYPE, MARKDOWN_TYPE].includes(dragging.type)) {
       const newEntitiesArray = Object.values(newEntities);
       const component = newEntitiesArray.find(entity => entity.type === dragging.type);
-      const parentColumn = newEntities[destination.id];
 
       newEntities[component.id] = {
         ...component,
         meta: {
           ...component.meta,
-          width: parentColumn.meta.width,
+          width: GRID_MIN_COLUMN_COUNT,
         },
       };
     }
diff --git a/superset/assets/javascripts/dashboard/v2/reducers/index.js b/superset/assets/javascripts/dashboard/v2/reducers/index.js
index 0134767..d6e9f84 100644
--- a/superset/assets/javascripts/dashboard/v2/reducers/index.js
+++ b/superset/assets/javascripts/dashboard/v2/reducers/index.js
@@ -1,10 +1,29 @@
-import undoable, { distinctState } from 'redux-undo';
+import undoable, { includeAction } from 'redux-undo';
+import {
+  UPDATE_COMPONENTS,
+  DELETE_COMPONENT,
+  CREATE_COMPONENT,
+  CREATE_TOP_LEVEL_TABS,
+  DELETE_TOP_LEVEL_TABS,
+  RESIZE_COMPONENT,
+  MOVE_COMPONENT,
+  HANDLE_COMPONENT_DROP,
+} from '../actions/dashboardLayout';
 
 import dashboardLayout from './dashboardLayout';
 
-export const undoableLayout = undoable(dashboardLayout, {
+const undoableLayout = undoable(dashboardLayout, {
   limit: 15,
-  filter: distinctState(),
+  filter: includeAction([
+    UPDATE_COMPONENTS,
+    DELETE_COMPONENT,
+    CREATE_COMPONENT,
+    CREATE_TOP_LEVEL_TABS,
+    DELETE_TOP_LEVEL_TABS,
+    RESIZE_COMPONENT,
+    MOVE_COMPONENT,
+    HANDLE_COMPONENT_DROP,
+  ]),
 });
 
 export default undoableLayout;
diff --git a/superset/assets/javascripts/dashboard/v2/stylesheets/components/chart.less b/superset/assets/javascripts/dashboard/v2/stylesheets/components/chart.less
index ce03797..8419b48 100644
--- a/superset/assets/javascripts/dashboard/v2/stylesheets/components/chart.less
+++ b/superset/assets/javascripts/dashboard/v2/stylesheets/components/chart.less
@@ -3,18 +3,33 @@
   height: 100%;
   color: @gray-dark;
   background-color: white;
-  padding: 16px;
-  display: flex;
-  align-items: center;
-  justify-content: center;
   position: relative;
+  overflow: hidden;
 }
 
-.dashboard-component-chart .fa {
-  //font-size: 100px;
-  opacity: 0.3;
+.dashboard-v2--editing .dashboard-component-chart {
+  border: 1px solid transparent;
 }
 
 .dashboard-v2--editing .dashboard-component-chart:hover {
-  box-shadow: inset 0 0 0 1px @gray-light;
+  border: 1px solid @indicator-color;
+}
+
+.dashboard-v2--editing .dashboard-component-chart .dashboard-chart .chart-container {
+  cursor: move;
+  opacity: 0.7;
+}
+
+.dashboard-v2--editing .dashboard-component-chart:hover .dashboard-chart .chart-container {
+  opacity: 1;
+}
+
+
+.dashboard-v2--editing .dashboard-component-chart .dashboard-chart .slice_container {
+  /* disable chart interactions in edit mode */
+  pointer-events: none;
+}
+
+.chart-header {
+  padding: 16px;
 }
diff --git a/superset/assets/javascripts/dashboard/v2/stylesheets/components/column.less b/superset/assets/javascripts/dashboard/v2/stylesheets/components/column.less
index 9565112..29fabc1 100644
--- a/superset/assets/javascripts/dashboard/v2/stylesheets/components/column.less
+++ b/superset/assets/javascripts/dashboard/v2/stylesheets/components/column.less
@@ -1,5 +1,6 @@
 .grid-column {
   width: 100%;
+  position: relative;
 }
 
 /* gutters between elements in a column */
@@ -8,12 +9,12 @@
 }
 
 .dashboard-v2--editing .grid-column:after {
-  border: 1px dashed transparent;
+  border: 1px solid transparent;
   content: "";
   position: absolute;
   width: 100%;
   height: 100%;
-  top: 1px;
+  top: 0;
   left: 0;
   z-index: 1;
   pointer-events: none;
diff --git a/superset/assets/javascripts/dashboard/v2/stylesheets/components/row.less b/superset/assets/javascripts/dashboard/v2/stylesheets/components/row.less
index 956966d..efc93fe 100644
--- a/superset/assets/javascripts/dashboard/v2/stylesheets/components/row.less
+++ b/superset/assets/javascripts/dashboard/v2/stylesheets/components/row.less
@@ -1,7 +1,8 @@
 .grid-row {
+  position: relative;
   display: flex;
   flex-direction: row;
-  flex-wrap: wrap;
+  flex-wrap: nowrap;
   align-items: flex-start;
   width: 100%;
   height: fit-content;
@@ -19,7 +20,7 @@
   position: absolute;
   width: 100%;
   height: 100%;
-  top: 1px;
+  top: 0;
   left: 0;
   z-index: 1;
   pointer-events: none;
diff --git a/superset/assets/javascripts/dashboard/v2/stylesheets/dnd.less b/superset/assets/javascripts/dashboard/v2/stylesheets/dnd.less
index 45a9784..835b62b 100644
--- a/superset/assets/javascripts/dashboard/v2/stylesheets/dnd.less
+++ b/superset/assets/javascripts/dashboard/v2/stylesheets/dnd.less
@@ -12,7 +12,7 @@
 
 /* drop indicators */
 .drop-indicator {
-  margin: auto;
+  display: block;
   background-color: @indicator-color;
   position: absolute;
   z-index: 10;
diff --git a/superset/assets/javascripts/dashboard/v2/stylesheets/grid.less b/superset/assets/javascripts/dashboard/v2/stylesheets/grid.less
index 45b8a42..a12ac97 100644
--- a/superset/assets/javascripts/dashboard/v2/stylesheets/grid.less
+++ b/superset/assets/javascripts/dashboard/v2/stylesheets/grid.less
@@ -1,12 +1,22 @@
 .grid-container {
+  min-height: 100%;
   position: relative;
   margin: 24px;
+  /* without this, the grid will not get smaller upon toggling the builder panel on */
+  min-width: 0;
+  width: 100%;
+}
+
+/* this is the ParentSize wrapper  */
+.grid-container > div:first-child {
+  height: inherit !important;
 }
 
 .grid-content {
-  height: 100%;
+  min-height: 100%;
   display: flex;
   flex-direction: column;
+  margin-bottom: 100px;
 }
 
 /* gutters between rows */
@@ -23,7 +33,7 @@
 .grid-column-guide {
   position: absolute;
   top: 0;
-  height: 100%;
+  min-height: 100%;
   background-color: rgba(68, 192, 255, 0.05);
   pointer-events: none;
   box-shadow: inset 0 0 0 1px rgba(68, 192, 255, 0.5);
diff --git a/superset/assets/javascripts/dashboard/v2/stylesheets/resizable.less b/superset/assets/javascripts/dashboard/v2/stylesheets/resizable.less
index 7bdd5f8..973daab 100644
--- a/superset/assets/javascripts/dashboard/v2/stylesheets/resizable.less
+++ b/superset/assets/javascripts/dashboard/v2/stylesheets/resizable.less
@@ -16,6 +16,7 @@
 
 .resize-handle {
   opacity: 0;
+  z-index: 10;
 }
 
   .resizable-container:hover .resize-handle,
@@ -35,26 +36,43 @@
   height: 8px;
 }
 
+
 .resize-handle--right {
   width: 2px;
   height: 20px;
-  right: 2px;
-  top: ~"calc(50% - 9px)"; /* escape for .less */
+  right: 4px;
+  top: 50%;
+  transform: translate(0, -50%);
   position: absolute;
   border-left: 1px solid @gray;
   border-right: 1px solid @gray;
 }
 
+.dragdroppable-column .resizable-container-handle--right {
+  /* override the default because the inner column's handle's mouse target is very small */
+  right: -10px !important;
+}
+
+.dragdroppable-column .dragdroppable-column .resizable-container-handle--right {
+  /* override the default because the inner column's handle's mouse target is very small */
+  right: 0px !important;
+}
+
 .resize-handle--bottom {
   height: 2px;
   width: 20px;
-  bottom: 2px;
-  left: ~"calc(50% - 10px)"; /* escape for .less */
+  bottom: 4px;
+  left: 50%;
+  transform: translate(-50%);
   position: absolute;
   border-top: 1px solid @gray;
   border-bottom: 1px solid @gray;
 }
 
+.resizable-container-handle--bottom {
+  bottom: 0 !important;
+}
+
 .resizable-container--resizing > span .resize-handle {
   border-color: @indicator-color;
 }
diff --git a/superset/assets/javascripts/dashboard/v2/util/charts/getEffectiveExtraFilters.js b/superset/assets/javascripts/dashboard/v2/util/charts/getEffectiveExtraFilters.js
new file mode 100644
index 0000000..e6b5c5e
--- /dev/null
+++ b/superset/assets/javascripts/dashboard/v2/util/charts/getEffectiveExtraFilters.js
@@ -0,0 +1,41 @@
+export default function getEffectiveExtraFilters({
+  dashboardMetadata,
+  filters,
+  sliceId,
+}) {
+  const immuneSlices = dashboardMetadata.filter_immune_slices || [];
+
+  const effectiveFilters = [];
+
+  if (sliceId && immuneSlices.includes(sliceId)) {
+    // The slice is immune to dashboard filters
+    return effectiveFilters;
+  }
+
+  // Build a list of fields the slice is immune to filters on
+  let immuneToFields = [];
+  if (
+    sliceId &&
+    dashboardMetadata.filter_immune_slice_fields &&
+    dashboardMetadata.filter_immune_slice_fields[sliceId]) {
+    immuneToFields = dashboardMetadata.filter_immune_slice_fields[sliceId];
+  }
+
+  Object.keys(filters).forEach((filteringSliceId) => {
+    if (filteringSliceId === sliceId.toString()) {
+      // Filters applied by the slice don't apply to itself
+      return;
+    }
+    Object.keys(filters[filteringSliceId]).forEach((field) => {
+      if (!immuneToFields.includes(field)) {
+        effectiveFilters.push({
+          col: field,
+          op: 'in',
+          val: filters[filteringSliceId][field],
+        });
+      }
+    });
+  });
+
+  return effectiveFilters;
+}
diff --git a/superset/assets/javascripts/dashboard/v2/util/charts/getFormDataWithExtraFilters.js b/superset/assets/javascripts/dashboard/v2/util/charts/getFormDataWithExtraFilters.js
new file mode 100644
index 0000000..ebb66e3
--- /dev/null
+++ b/superset/assets/javascripts/dashboard/v2/util/charts/getFormDataWithExtraFilters.js
@@ -0,0 +1,40 @@
+import getEffectiveExtraFilters from './getEffectiveExtraFilters';
+
+// We cache formData objects so that our connected container components don't always trigger
+// render cascades. we cannot leverage the reselect library because our cache size is >1
+let cachedMetadata = null;
+let cachedFormdata = {};
+
+export default function getFormDataWithExtraFilters({
+  chart,
+  dashboardMetadata,
+  filters,
+  sliceId,
+}) {
+  // dashboard metadata has not changed use cache if possible
+  if (cachedMetadata === dashboardMetadata && cachedFormdata[sliceId]) {
+    return cachedFormdata[sliceId];
+  } else if (cachedMetadata !== dashboardMetadata) {
+    // changes to dashboardMetadata should invalidate all caches
+    cachedMetadata = dashboardMetadata;
+    cachedFormdata = {};
+  }
+
+  const extraFilters = getEffectiveExtraFilters({
+    dashboardMetadata,
+    filters,
+    sliceId,
+  });
+
+  const formData = {
+    ...chart.formData,
+    extra_filters: [
+      ...chart.formData.filters,
+      ...extraFilters,
+    ],
+  };
+
+  cachedFormdata[sliceId] = formData;
+
+  return formData;
+}
diff --git a/superset/assets/javascripts/dashboard/v2/util/dropOverflowsParent.js b/superset/assets/javascripts/dashboard/v2/util/dropOverflowsParent.js
index 0fd0c4e..e298719 100644
--- a/superset/assets/javascripts/dashboard/v2/util/dropOverflowsParent.js
+++ b/superset/assets/javascripts/dashboard/v2/util/dropOverflowsParent.js
@@ -1,13 +1,18 @@
 import { COLUMN_TYPE } from '../util/componentTypes';
-import { GRID_COLUMN_COUNT, NEW_COMPONENTS_SOURCE_ID } from './constants';
+import { GRID_COLUMN_COUNT, NEW_COMPONENTS_SOURCE_ID, GRID_MIN_COLUMN_COUNT } from './constants';
 import findParentId from './findParentId';
 import getChildWidth from './getChildWidth';
 import newComponentFactory from './newComponentFactory';
 
 export default function doesChildOverflowParent(dropResult, components) {
   const { source, destination, dragging } = dropResult;
-  const isNewComponent = source.id === NEW_COMPONENTS_SOURCE_ID;
 
+  // moving a component within a container should never overflow
+  if (source.id === destination.id) {
+    return false;
+  }
+
+  const isNewComponent = source.id === NEW_COMPONENTS_SOURCE_ID;
   const grandparentId = findParentId({ childId: destination.id, components });
 
   const child = isNewComponent ? newComponentFactory(dragging.type) : components[dragging.id] || {};
@@ -17,7 +22,8 @@ export default function doesChildOverflowParent(dropResult, components) {
   const grandparentWidth = (grandparent.meta && grandparent.meta.width) || GRID_COLUMN_COUNT;
   const parentWidth = (parent.meta && parent.meta.width) || grandparentWidth;
   const parentChildWidth = parent.type === COLUMN_TYPE
-    ? 0 : getChildWidth({ id: destination.id, components });
+    ? (parent.meta && parent.meta.width) || GRID_MIN_COLUMN_COUNT
+    : getChildWidth({ id: destination.id, components });
   const childWidth = (child.meta && child.meta.width) || 0;
 
   return parentWidth - parentChildWidth < childWidth;
diff --git a/superset/assets/javascripts/dashboard/v2/util/getDropPosition.js b/superset/assets/javascripts/dashboard/v2/util/getDropPosition.js
index 9605db2..b0a75fb 100644
--- a/superset/assets/javascripts/dashboard/v2/util/getDropPosition.js
+++ b/superset/assets/javascripts/dashboard/v2/util/getDropPosition.js
@@ -8,7 +8,7 @@ export const DROP_LEFT = 'DROP_LEFT';
 
 // this defines how close the mouse must be to the edge of a component to display
 // a sibling type drop indicator
-const SIBLING_DROP_THRESHOLD = 15;
+const SIBLING_DROP_THRESHOLD = 20;
 
 export default function getDropPosition(monitor, Component) {
   const {
diff --git a/superset/assets/javascripts/explore/components/ExploreChartPanel.jsx b/superset/assets/javascripts/explore/components/ExploreChartPanel.jsx
index f595fb5..6821185 100644
--- a/superset/assets/javascripts/explore/components/ExploreChartPanel.jsx
+++ b/superset/assets/javascripts/explore/components/ExploreChartPanel.jsx
@@ -38,15 +38,15 @@ class ExploreChartPanel extends React.PureComponent {
   }
 
   renderChart() {
-    debugger
+    const { chart } = this.props;
     return (
       <ChartContainer
+        sliceId={chart.chartKey}
         containerId={this.props.containerId}
         datasource={this.props.datasource}
         formData={this.props.form_data}
         height={this.getHeight()}
         slice={this.props.slice}
-        chart={this.props.chart}
         setControlValue={this.props.actions.setControlValue}
         timeout={this.props.timeout}
         vizType={this.props.vizType}
@@ -54,6 +54,16 @@ class ExploreChartPanel extends React.PureComponent {
         errorMessage={this.props.errorMessage}
         onQuery={this.props.onQuery}
         onDismissRefreshOverlay={this.props.onDismissRefreshOverlay}
+        annotationData={chart.annotationData}
+        chartAlert={chart.chartAlert}
+        chartStatus={chart.chartStatus}
+        chartUpdateEndTime={chart.chartUpdateEndTime}
+        chartUpdateStartTime={chart.chartUpdateStartTime}
+        latestQueryFormData={chart.latestQueryFormData}
+        lastRendered={chart.lastRendered}
+        queryResponse={chart.queryResponse}
+        queryRequest={chart.queryRequest}
+        triggerQuery={chart.triggerQuery}
       />
     );
   }
diff --git a/superset/assets/package.json b/superset/assets/package.json
index c3afd7a..27fe935 100644
--- a/superset/assets/package.json
+++ b/superset/assets/package.json
@@ -107,7 +107,7 @@
     "redux": "^3.5.2",
     "redux-localstorage": "^0.4.1",
     "redux-thunk": "^2.1.0",
-    "redux-undo": "^0.6.1",
+    "redux-undo": "^1.0.0-beta9-9-7",
     "shortid": "^2.2.6",
     "sprintf-js": "^1.1.1",
     "srcdoc-polyfill": "^1.0.0",
diff --git a/superset/assets/stylesheets/dashboard.less b/superset/assets/stylesheets/dashboard.less
index a8973a3..f9c9b3d 100644
--- a/superset/assets/stylesheets/dashboard.less
+++ b/superset/assets/stylesheets/dashboard.less
@@ -109,22 +109,6 @@
   display: none;
 }
 
-.slice-grid div.separator.widget {
- border: 1px solid transparent;
-  box-shadow: none;
-  z-index: 1;
-}
-.slice-grid div.separator.widget:hover {
-  border: 1px solid #EEE;
-}
-.slice-grid div.separator.widget .chart-header {
-  background-color: transparent;
-  color: transparent;
-}
-.slice-grid div.separator.widget h1,h2,h3,h4 {
-  margin-top: 0px;
-}
-
 .slice-cell {
   box-shadow: 0px 0px 20px 5px rgba(0,0,0,0);
   transition: box-shadow 1s ease-in;
@@ -142,7 +126,7 @@
   height: 100%;
 }
 
-.slice-cell .editable-title input[type="button"] {
+.dashboard-chart .editable-title input[type="button"] {
   font-weight: bold;
 }
 
@@ -302,4 +286,4 @@ i.warning {
   .ReactVirtualized__Grid.ReactVirtualized__List:focus {
     outline: none;
   }
-}
\ No newline at end of file
+}
diff --git a/superset/assets/visualizations/nvd3_vis.css b/superset/assets/visualizations/nvd3_vis.css
index fed0d01..6b3b25d 100644
--- a/superset/assets/visualizations/nvd3_vis.css
+++ b/superset/assets/visualizations/nvd3_vis.css
@@ -11,10 +11,6 @@ text.nv-axislabel {
   font-size: 14px;
 }
 
-.slice_container.dist_bar {
-  overflow-x: auto !important;
-}
-
 .dist_bar svg.nvd3-svg {
   width: auto;
   font-size: 14px;
@@ -63,4 +59,3 @@ g.opacityMedium path, line.opacityMedium {
 g.opacityHigh path, line.opacityHigh {
   stroke-opacity: .8
 }
-
diff --git a/superset/templates/superset/dashboard.html b/superset/templates/superset/dashboard.html
index 1a158d9..5c93d2a 100644
--- a/superset/templates/superset/dashboard.html
+++ b/superset/templates/superset/dashboard.html
@@ -1,10 +1,5 @@
 {% extends "superset/basic.html" %}
 
 {% block body %}
-<div
-  id="app"
-  class="dashboard container-fluid"
-  data-bootstrap="{{ bootstrap_data }}"
->
-</div>
+  <div id="app" class="dashboard" data-bootstrap="{{ bootstrap_data }}" />
 {% endblock %}

-- 
To stop receiving notification emails like this one, please contact
ccwilliams@apache.org.