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:32 UTC

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

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.