superset-commits mailing list archives

Site index · List index
Message view « Date » · « Thread »
Top « Date » · « Thread »
From grace...@apache.org
Subject [incubator-superset] branch dashboard-builder updated: [dashboard builder] improve perf (#4855)
Date Tue, 24 Apr 2018 18:07:26 GMT
This is an automated email from the ASF dual-hosted git repository.

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


The following commit(s) were added to refs/heads/dashboard-builder by this push:
     new 791cf00  [dashboard builder] improve perf (#4855)
791cf00 is described below

commit 791cf00a73743ed29fa6460138ef41a14248284f
Author: Chris Williams <williaster@users.noreply.github.com>
AuthorDate: Tue Apr 24 11:07:23 2018 -0700

    [dashboard builder] improve perf (#4855)
    
    * address major perf + css issues
    
    [dashboard builder] fix dashboard filters and some css
    
    [dashboard builder] use VIZ_TYPES, move stricter .eslintrc to dashboard/, more css fixes
    
    [builder] delete GridCell and GridLayout, remove some unused css. fix broken tabs.
    
    * [builder] fix errors post-rebase
    
    * [builder] add support for custom DragDroppable drag layer and add AddSliceDragPreview
    
    * [AddSliceDragPreview] fix type check
    
    * [dashboard builder] add prettier and update all files
    
    * [dashboard builder] merge v2/ directory int dashboard/
    
    * [dashboard builder] move component/*Container => containers/*
---
 superset/assets/images/loading.gif                 | Bin 16671 -> 1945878 bytes
 superset/assets/package.json                       |   5 +-
 superset/assets/src/chart/Chart.jsx                |  80 +-
 superset/assets/src/chart/ChartContainer.jsx       |  22 +-
 superset/assets/src/chart/chartReducer.js          |   5 +-
 superset/assets/src/components/Loading.jsx         |   3 +
 superset/assets/src/dashboard/{v2 => }/.eslintrc   |   6 +-
 superset/assets/src/dashboard/.prettierrc          |   4 +
 .../dashboard/{v2 => }/actions/dashboardLayout.js  |  59 +-
 .../assets/src/dashboard/actions/dashboardState.js |  58 +-
 .../assets/src/dashboard/actions/datasources.js    |   3 +-
 .../dashboard/{v2 => }/actions/messageToasts.js    |  13 +-
 .../assets/src/dashboard/actions/sliceEntities.js  |  15 +-
 .../src/dashboard/components/ActionMenuItem.jsx    |  40 +-
 .../src/dashboard/components/AddSliceCard.jsx      |  59 ++
 .../{v2 => }/components/BuilderComponentPane.jsx   |  15 +-
 .../assets/src/dashboard/components/CodeModal.jsx  |  10 +-
 .../assets/src/dashboard/components/Controls.jsx   | 123 +--
 .../assets/src/dashboard/components/CssEditor.jsx  |  11 +-
 .../assets/src/dashboard/components/Dashboard.jsx  | 240 ++---
 .../{v2 => }/components/DashboardBuilder.jsx       |  39 +-
 .../dashboard/components/DashboardContainer.jsx    |  51 --
 .../{v2 => }/components/DashboardGrid.jsx          | 106 ++-
 .../{v2 => }/components/DeleteComponentButton.jsx  |   7 +-
 .../assets/src/dashboard/components/GridCell.jsx   | 158 ----
 .../assets/src/dashboard/components/GridLayout.jsx | 156 ----
 .../assets/src/dashboard/components/Header.jsx     | 143 +--
 .../dashboard/{v2 => }/components/IconButton.jsx   |   0
 .../dashboard/components/RefreshIntervalModal.jsx  |   4 +-
 .../assets/src/dashboard/components/SaveModal.jsx  |  63 +-
 .../assets/src/dashboard/components/SliceAdder.jsx | 125 +--
 .../dashboard/components/SliceAdderContainer.jsx   |  25 -
 .../src/dashboard/components/SliceHeader.jsx       |  99 +-
 .../dashboard/components/SliceHeaderControls.jsx   |  29 +-
 .../src/dashboard/{v2 => }/components/Toast.jsx    |  11 +-
 .../{v2 => }/components/ToastPresenter.jsx         |   9 +-
 .../components/dnd/AddSliceDragPreview.jsx         |  70 ++
 .../{v2 => }/components/dnd/DragDroppable.jsx      |  52 +-
 .../{v2 => }/components/dnd/DragHandle.jsx         |   8 +-
 .../{v2 => }/components/dnd/dragDroppableConfig.js |  12 +-
 .../{v2 => }/components/dnd/handleDrop.js          |  22 +-
 .../{v2 => }/components/dnd/handleHover.js         |   0
 .../dashboard/components/gridComponents/Chart.jsx  | 233 +++++
 .../components/gridComponents/ChartHolder.jsx      |  63 +-
 .../{v2 => }/components/gridComponents/Column.jsx  |  55 +-
 .../{v2 => }/components/gridComponents/Divider.jsx |   5 +-
 .../{v2 => }/components/gridComponents/Header.jsx  |  18 +-
 .../{v2 => }/components/gridComponents/Row.jsx     |  48 +-
 .../{v2 => }/components/gridComponents/Tab.jsx     |  26 +-
 .../{v2 => }/components/gridComponents/Tabs.jsx    |  61 +-
 .../{v2 => }/components/gridComponents/index.js    |   2 -
 .../gridComponents/new/DraggableNewComponent.jsx   |   5 +-
 .../components/gridComponents/new/NewColumn.jsx    |  16 +
 .../components/gridComponents/new/NewDivider.jsx   |  16 +
 .../components/gridComponents/new/NewHeader.jsx    |  16 +
 .../components/gridComponents/new/NewRow.jsx       |  16 +
 .../components/gridComponents/new/NewTabs.jsx      |  16 +
 .../components/menu/BackgroundStyleDropdown.jsx    |   0
 .../{v2 => }/components/menu/HoverMenu.jsx         |   0
 .../{v2 => }/components/menu/PopoverDropdown.jsx   |   4 +-
 .../{v2 => }/components/menu/WithPopoverMenu.jsx   |  22 +-
 .../components/resizable/ResizableContainer.jsx    |  72 +-
 .../components/resizable/ResizableHandle.jsx       |  12 +-
 superset/assets/src/dashboard/containers/Chart.jsx |  59 ++
 .../assets/src/dashboard/containers/Dashboard.jsx  |  49 +
 .../{v2 => }/containers/DashboardBuilder.jsx       |  18 +-
 .../{v2 => }/containers/DashboardComponent.jsx     |  44 +-
 .../{v2 => }/containers/DashboardGrid.jsx          |  16 +-
 .../{v2 => }/containers/DashboardHeader.jsx        |  48 +-
 .../{v2 => }/containers/ToastPresenter.jsx         |   0
 .../{v2 => }/fixtures/emptyDashboardLayout.js      |   4 +-
 superset/assets/src/dashboard/index.jsx            |   7 +-
 .../dashboard/{v2 => }/reducers/dashboardLayout.js |  80 +-
 .../src/dashboard/reducers/dashboardState.js       |  25 +-
 .../assets/src/dashboard/reducers/datasources.js   |   9 +-
 .../src/dashboard/reducers/getInitialState.js      |  26 +-
 superset/assets/src/dashboard/reducers/index.js    |  12 +-
 .../dashboard/{v2 => }/reducers/messageToasts.js   |   4 +-
 .../assets/src/dashboard/reducers/sliceEntities.js |  10 +-
 .../dashboard/reducers/undoableDashboardLayout.js  |  27 +
 .../{v2 => }/stylesheets/builder-sidepane.less     |  28 +-
 .../dashboard/{v2 => }/stylesheets/builder.less    |  23 +-
 .../dashboard/{v2 => }/stylesheets/buttons.less    |   0
 .../dashboard/stylesheets/components/chart.less    |  69 ++
 .../{v2 => }/stylesheets/components/column.less    |  19 +-
 .../{v2 => }/stylesheets/components/divider.less   |   0
 .../{v2 => }/stylesheets/components/header.less    |   5 +-
 .../{v2 => }/stylesheets/components/index.less     |   0
 .../stylesheets/components/new-component.less      |   0
 .../{v2 => }/stylesheets/components/row.less       |  19 +-
 .../{v2 => }/stylesheets/components/tabs.less      |   0
 .../src/dashboard/stylesheets/dashboard.less       | 104 +++
 .../src/dashboard/{v2 => }/stylesheets/dnd.less    |   2 +-
 .../src/dashboard/{v2 => }/stylesheets/grid.less   |  14 +-
 .../dashboard/{v2 => }/stylesheets/hover-menu.less |   0
 .../src/dashboard/{v2 => }/stylesheets/index.less  |   2 +
 .../{v2 => }/stylesheets/popover-menu.less         |   0
 .../dashboard/{v2 => }/stylesheets/resizable.less  |  26 +-
 .../src/dashboard/{v2 => }/stylesheets/toast.less  |   5 +-
 .../dashboard/{v2 => }/stylesheets/variables.less  |   0
 .../src/dashboard/util/backgroundStyleOptions.js   |  15 +
 .../util/charts/getEffectiveExtraFilters.js        |  42 +
 .../util/charts/getFormDataWithExtraFilters.js     |  42 +
 .../src/dashboard/util/componentIsResizable.js     |   5 +
 .../src/dashboard/{v2 => }/util/componentTypes.js  |   0
 .../src/dashboard/{v2 => }/util/constants.js       |   0
 .../assets/src/dashboard/util/dashboardHelper.js   |   9 -
 .../src/dashboard/util/dashboardLayoutConverter.js |  93 +-
 .../src/dashboard/{v2 => }/util/dnd-reorder.js     |  12 +-
 .../dashboard/{v2 => }/util/dropOverflowsParent.js |  26 +-
 .../src/dashboard/{v2 => }/util/findParentId.js    |   6 +-
 .../src/dashboard/util/getChartIdsFromLayout.js    |   8 +
 .../src/dashboard/{v2 => }/util/getChildWidth.js   |   6 +-
 .../src/dashboard/{v2 => }/util/getDropPosition.js |  26 +-
 .../dashboard/{v2 => }/util/headerStyleOptions.js  |   2 +-
 .../src/dashboard/{v2 => }/util/isValidChild.js    |  28 +-
 .../dashboard/{v2 => }/util/newComponentFactory.js |  10 +-
 .../dashboard/{v2 => }/util/newEntitiesFromDrop.js |  14 +-
 .../src/dashboard/{v2 => }/util/propShapes.jsx     |  20 +-
 .../src/dashboard/{v2 => }/util/resizableConfig.js |   0
 .../{v2 => }/util/shouldWrapChildInRow.js          |   0
 .../assets/src/dashboard/v2/actions/editMode.js    |   9 -
 .../src/dashboard/v2/components/Dashboard.jsx      |  30 -
 .../dashboard/v2/components/DashboardHeader.jsx    |  99 --
 .../dashboard/v2/components/StaticDashboard.jsx    |  19 -
 .../v2/components/gridComponents/new/NewChart.jsx  |  24 -
 .../v2/components/gridComponents/new/NewColumn.jsx |  24 -
 .../components/gridComponents/new/NewDivider.jsx   |  24 -
 .../v2/components/gridComponents/new/NewHeader.jsx |  24 -
 .../v2/components/gridComponents/new/NewRow.jsx    |  23 -
 .../v2/components/gridComponents/new/NewTabs.jsx   |  24 -
 .../assets/src/dashboard/v2/reducers/editMode.js   |  11 -
 superset/assets/src/dashboard/v2/reducers/index.js |   8 -
 .../dashboard/v2/stylesheets/components/chart.less |  20 -
 .../dashboard/v2/util/backgroundStyleOptions.js    |   7 -
 .../src/dashboard/v2/util/componentIsResizable.js  |  13 -
 .../src/explore/components/ExploreChartHeader.jsx  |   2 +-
 .../src/explore/components/ExploreChartPanel.jsx   |  15 +-
 .../explore/components/ExploreViewContainer.jsx    |   2 +-
 superset/assets/src/visualizations/nvd3_vis.css    |   1 -
 superset/assets/stylesheets/dashboard.less         | 156 ----
 superset/assets/stylesheets/superset.less          |   2 +-
 superset/assets/stylesheets/welcome.css            |   2 +-
 superset/assets/yarn.lock                          | 994 ++++++++++++++++++---
 superset/templates/superset/dashboard.html         |   7 +-
 superset/views/core.py                             |   2 +-
 146 files changed, 3220 insertions(+), 2106 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/package.json b/superset/assets/package.json
index b458466..de40936 100644
--- a/superset/assets/package.json
+++ b/superset/assets/package.json
@@ -112,7 +112,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",
@@ -135,8 +135,10 @@
     "enzyme": "^2.0.0",
     "eslint": "^4.19.0",
     "eslint-config-airbnb": "^15.0.1",
+    "eslint-config-prettier": "^2.9.0",
     "eslint-plugin-import": "^2.2.0",
     "eslint-plugin-jsx-a11y": "^5.1.1",
+    "eslint-plugin-prettier": "^2.6.0",
     "eslint-plugin-react": "^7.0.1",
     "exports-loader": "^0.6.3",
     "extract-text-webpack-plugin": "3.0.0",
@@ -151,6 +153,7 @@
     "less-loader": "^4.0.3",
     "mocha": "^3.2.0",
     "npm-check-updates": "^2.14.0",
+    "prettier": "^1.12.1",
     "react-addons-test-utils": "^15.6.2",
     "react-test-renderer": "^15.6.2",
     "redux-mock-store": "^1.2.3",
diff --git a/superset/assets/src/chart/Chart.jsx b/superset/assets/src/chart/Chart.jsx
index e208835..a76891c 100644
--- a/superset/assets/src/chart/Chart.jsx
+++ b/superset/assets/src/chart/Chart.jsx
@@ -73,10 +73,16 @@ class Chart extends React.PureComponent {
 
   componentDidMount() {
     if (this.props.triggerQuery) {
-      this.props.actions.runQuery(this.props.formData, false,
+      const { formData } = this.props;
+      this.props.actions.runQuery(
+        formData,
+        false,
         this.props.timeout,
         this.props.chartId,
       );
+    } else {
+      // when drag/dropping in a dashboard, a chart may be unmounted/remounted but still have data
+      this.renderViz();
     }
   }
 
@@ -89,14 +95,16 @@ class Chart extends React.PureComponent {
   }
 
   componentDidUpdate(prevProps) {
-    if (this.props.queryResponse &&
+    if (
+      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();
     }
@@ -123,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() {
@@ -131,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) {
@@ -159,7 +169,6 @@ class Chart extends React.PureComponent {
 
   renderTooltip() {
     if (this.state.tooltip) {
-      /* eslint-disable react/no-danger */
       return (
         <Tooltip
           className="chart-tooltip"
@@ -169,52 +178,55 @@ 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];
-    const fd = this.props.formData;
-    const qr = this.props.queryResponse;
+    const { vizType, formData, queryResponse, setControlValue, chartId, 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(chartId);
       }
-      // [re]rendering the visualization
-      viz(this, qr, this.props.setControlValue);
       Logger.append(LOG_ACTIONS_RENDER_EVENT, {
-        label: 'slice_' + this.props.chartId,
-        vis_type: this.props.vizType,
+        label: 'slice_' + chartId,
+        vis_type: vizType,
         start_offset: renderStart,
         duration: Logger.getTimestamp() - renderStart,
       });
-      this.props.actions.chartRenderingSucceeded(this.props.chartId);
+      this.props.actions.chartRenderingSucceeded(chartId);
     } catch (e) {
-      this.props.actions.chartRenderingFailed(e, this.props.chartId);
+      console.error(e); // eslint-disable-line no-console
+      this.props.actions.chartRenderingFailed(e, chartId);
     }
   }
 
   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={`${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 &&
@@ -226,8 +238,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/src/chart/ChartContainer.jsx b/superset/assets/src/chart/ChartContainer.jsx
index e3cb1f9..b66fe5d 100644
--- a/superset/assets/src/chart/ChartContainer.jsx
+++ b/superset/assets/src/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({ charts }, ownProps) {
-  const chart = charts[ownProps.chartId];
-  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/src/chart/chartReducer.js b/superset/assets/src/chart/chartReducer.js
index d57959a..ea8de8b 100644
--- a/superset/assets/src/chart/chartReducer.js
+++ b/superset/assets/src/chart/chartReducer.js
@@ -142,7 +142,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/src/components/Loading.jsx b/superset/assets/src/components/Loading.jsx
index 416e770..810c581 100644
--- a/superset/assets/src/components/Loading.jsx
+++ b/superset/assets/src/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/src/dashboard/v2/.eslintrc b/superset/assets/src/dashboard/.eslintrc
similarity index 84%
rename from superset/assets/src/dashboard/v2/.eslintrc
rename to superset/assets/src/dashboard/.eslintrc
index 70efc15..a3f86e3 100644
--- a/superset/assets/src/dashboard/v2/.eslintrc
+++ b/superset/assets/src/dashboard/.eslintrc
@@ -1,4 +1,6 @@
 {
+  "extends": "prettier",
+  "plugins": ["prettier"],
   "rules": {
     "prefer-template": 2,
     "new-cap": 2,
@@ -12,7 +14,7 @@
     "jsx-a11y/anchor-has-content": 2,
     "react/require-default-props": 2,
     "no-plusplus": 2,
-    "no-mixed-operators": 2,
+    "no-mixed-operators": 0,
     "no-continue": 2,
     "no-bitwise": 2,
     "no-undef": 2,
@@ -25,5 +27,7 @@
     "import/prefer-default-export": 2,
     "react/no-unescaped-entities": 2,
     "react/no-string-refs": 2,
+    "react/jsx-indent": 0,
+    "prettier/prettier": "error"
   }
 }
diff --git a/superset/assets/src/dashboard/.prettierrc b/superset/assets/src/dashboard/.prettierrc
new file mode 100644
index 0000000..a20502b
--- /dev/null
+++ b/superset/assets/src/dashboard/.prettierrc
@@ -0,0 +1,4 @@
+{
+  "singleQuote": true,
+  "trailingComma": "all"
+}
diff --git a/superset/assets/src/dashboard/v2/actions/dashboardLayout.js b/superset/assets/src/dashboard/actions/dashboardLayout.js
similarity index 73%
rename from superset/assets/src/dashboard/v2/actions/dashboardLayout.js
rename to superset/assets/src/dashboard/actions/dashboardLayout.js
index b6d41c4..5a04de5 100644
--- a/superset/assets/src/dashboard/v2/actions/dashboardLayout.js
+++ b/superset/assets/src/dashboard/actions/dashboardLayout.js
@@ -1,6 +1,11 @@
 import { addInfoToast } from './messageToasts';
+import { setUnsavedChanges } from './dashboardState';
 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 +67,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,14 +82,16 @@ export function resizeComponent({ id, width, height }) {
         },
       };
 
-      component.children.forEach((childId) => {
+      // 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)) {
           updatedComponents[childId] = {
             ...child,
             meta: {
               ...child.meta,
-              width: width || child.meta.width,
+              width: GRID_MIN_COLUMN_COUNT,
               height: height || child.meta.height,
             },
           };
@@ -94,6 +99,7 @@ export function resizeComponent({ id, width, height }) {
       });
 
       dispatch(updateComponents(updatedComponents));
+      dispatch(setUnsavedChanges(true));
     }
   };
 }
@@ -112,13 +118,18 @@ export function moveComponent(dropResult) {
 export const HANDLE_COMPONENT_DROP = 'HANDLE_COMPONENT_DROP';
 export function handleComponentDrop(dropResult) {
   return (dispatch, getState) => {
-    const overflowsParent = dropOverflowsParent(dropResult, getState().dashboardLayout.present);
+    const overflowsParent = dropOverflowsParent(
+      dropResult,
+      getState().dashboardLayout.present,
+    );
 
     if (overflowsParent) {
-      return dispatch(addInfoToast(
-        `Parent does not have enough space for this component.
+      return dispatch(
+        addInfoToast(
+          `Parent does not have enough space for this component.
          Try decreasing its width or add it to a new row.`,
-      ));
+        ),
+      );
     }
 
     const { source, destination } = dropResult;
@@ -130,12 +141,10 @@ export function handleComponentDrop(dropResult) {
     } else if (destination && isNewComponent) {
       dispatch(createComponent(dropResult));
     } else if (
-      destination
-      && source
-      && !( // ensure it has moved
-        destination.id === source.id
-        && destination.index === source.index
-      )
+      destination &&
+      source &&
+      !// ensure it has moved
+      (destination.id === source.id && destination.index === source.index)
     ) {
       dispatch(moveComponent(dropResult));
     }
@@ -146,12 +155,20 @@ export function handleComponentDrop(dropResult) {
       const { present: layout } = undoableLayout;
       const sourceComponent = layout[source.id];
 
-      if (sourceComponent.type === TABS_TYPE && sourceComponent.children.length === 0) {
-        const parentId = findParentId({ childId: source.id, components: layout });
+      if (
+        sourceComponent.type === TABS_TYPE &&
+        sourceComponent.children.length === 0
+      ) {
+        const parentId = findParentId({
+          childId: source.id,
+          components: layout,
+        });
         dispatch(deleteComponent(source.id, parentId));
       }
     }
 
+    dispatch(setUnsavedChanges(true));
+
     return null;
   };
 }
diff --git a/superset/assets/src/dashboard/actions/dashboardState.js b/superset/assets/src/dashboard/actions/dashboardState.js
index 2262729..d80ec83 100644
--- a/superset/assets/src/dashboard/actions/dashboardState.js
+++ b/superset/assets/src/dashboard/actions/dashboardState.js
@@ -6,6 +6,11 @@ import { chart as initChart } from '../../chart/chartReducer';
 import { fetchDatasourceMetadata } from '../../dashboard/actions/datasources';
 import { applyDefaultFormData } from '../../explore/stores/store';
 
+export const SET_UNSAVED_CHANGES = 'SET_UNSAVED_CHANGES';
+export function setUnsavedChanges(hasUnsavedChanges) {
+  return { type: SET_UNSAVED_CHANGES, payload: { hasUnsavedChanges } };
+}
+
 export const ADD_FILTER = 'ADD_FILTER';
 export function addFilter(chart, col, vals, merge = true, refresh = true) {
   return { type: ADD_FILTER, chart, col, vals, merge, refresh };
@@ -39,20 +44,19 @@ export function toggleFaveStar(isStarred) {
 
 export const FETCH_FAVE_STAR = 'FETCH_FAVE_STAR';
 export function fetchFaveStar(id) {
-  return function (dispatch) {
+  return function fetchFaveStarThunk(dispatch) {
     const url = `${FAVESTAR_BASE_URL}/${id}/count`;
-    return $.get(url)
-      .done((data) => {
-        if (data.count > 0) {
-          dispatch(toggleFaveStar(true));
-        }
-      });
+    return $.get(url).done(data => {
+      if (data.count > 0) {
+        dispatch(toggleFaveStar(true));
+      }
+    });
   };
 }
 
 export const SAVE_FAVE_STAR = 'SAVE_FAVE_STAR';
 export function saveFaveStar(id, isStarred) {
-  return function (dispatch) {
+  return function saveFaveStarThunk(dispatch) {
     const urlSuffix = isStarred ? 'unselect' : 'select';
     const url = `${FAVESTAR_BASE_URL}/${id}/${urlSuffix}/`;
     $.get(url);
@@ -82,21 +86,29 @@ export function onSave() {
 
 export function fetchCharts(chartList = [], force = false, interval = 0) {
   return (dispatch, getState) => {
-    const timeout = getState().dashboardInfo.common.conf.SUPERSET_WEBSERVER_TIMEOUT;
+    const timeout = getState().dashboardInfo.common.conf
+      .SUPERSET_WEBSERVER_TIMEOUT;
     if (!interval) {
-      chartList.forEach(chart => (dispatch(refreshChart(chart, force, timeout))));
+      chartList.forEach(chart => dispatch(refreshChart(chart, force, timeout)));
       return;
     }
 
     const { metadata: meta } = getState().dashboardInfo;
     const refreshTime = Math.max(interval, meta.stagger_time || 5000); // default 5 seconds
     if (typeof meta.stagger_refresh !== 'boolean') {
-      meta.stagger_refresh = meta.stagger_refresh === undefined ?
-        true : meta.stagger_refresh === 'true';
+      meta.stagger_refresh =
+        meta.stagger_refresh === undefined
+          ? true
+          : meta.stagger_refresh === 'true';
     }
-    const delay = meta.stagger_refresh ? refreshTime / (chartList.length - 1) : 0;
+    const delay = meta.stagger_refresh
+      ? refreshTime / (chartList.length - 1)
+      : 0;
     chartList.forEach((chart, i) => {
-      setTimeout(() => dispatch(refreshChart(chart, force, timeout)), delay * i);
+      setTimeout(
+        () => dispatch(refreshChart(chart, force, timeout)),
+        delay * i,
+      );
     });
   };
 }
@@ -116,9 +128,9 @@ export function startPeriodicRender(interval) {
     const { metadata } = getState().dashboardInfo;
     const immune = metadata.timed_refresh_immune_slices || [];
     const refreshAll = () => {
-      const affected =
-        Object.values(getState().charts)
-          .filter(chart => immune.indexOf(chart.id) === -1);
+      const affected = Object.values(getState().charts).filter(
+        chart => immune.indexOf(chart.id) === -1,
+      );
       return dispatch(fetchCharts(affected, true, interval * 0.2));
     };
     const fetchAndRender = () => {
@@ -149,17 +161,15 @@ export function addSliceToDashboard(id) {
       formData: applyDefaultFormData(form_data),
     };
 
-    return Promise
-      .all([
-        dispatch(addChart(newChart, id)),
-        dispatch(fetchDatasourceMetadata(form_data.datasource)),
-      ])
-      .then(() => dispatch(addSlice(selectedSlice)));
+    return Promise.all([
+      dispatch(addChart(newChart, id)),
+      dispatch(fetchDatasourceMetadata(form_data.datasource)),
+    ]).then(() => dispatch(addSlice(selectedSlice)));
   };
 }
 
 export function removeSliceFromDashboard(chart) {
-  return (dispatch) => {
+  return dispatch => {
     dispatch(removeSlice(chart.id));
     dispatch(removeChart(chart.id));
   };
diff --git a/superset/assets/src/dashboard/actions/datasources.js b/superset/assets/src/dashboard/actions/datasources.js
index a00bb17..d97296e 100644
--- a/superset/assets/src/dashboard/actions/datasources.js
+++ b/superset/assets/src/dashboard/actions/datasources.js
@@ -29,7 +29,8 @@ export function fetchDatasourceMetadata(key) {
       type: 'GET',
       url,
       success: data => dispatch(setDatasource(data, key)),
-      error: error => dispatch(fetchDatasourceFailed(error.responseJSON.error, key)),
+      error: error =>
+        dispatch(fetchDatasourceFailed(error.responseJSON.error, key)),
     });
   };
 }
diff --git a/superset/assets/src/dashboard/v2/actions/messageToasts.js b/superset/assets/src/dashboard/actions/messageToasts.js
similarity index 85%
rename from superset/assets/src/dashboard/v2/actions/messageToasts.js
rename to superset/assets/src/dashboard/actions/messageToasts.js
index 2ebc06c..367b36f 100644
--- a/superset/assets/src/dashboard/v2/actions/messageToasts.js
+++ b/superset/assets/src/dashboard/actions/messageToasts.js
@@ -1,7 +1,16 @@
-import { INFO_TOAST, SUCCESS_TOAST, WARNING_TOAST, DANGER_TOAST } from '../util/constants';
+import {
+  INFO_TOAST,
+  SUCCESS_TOAST,
+  WARNING_TOAST,
+  DANGER_TOAST,
+} from '../util/constants';
 
 function getToastUuid(type) {
-  return `${Math.random().toString(16).slice(2)}-${type}-${Math.random().toString(16).slice(2)}`;
+  return `${Math.random()
+    .toString(16)
+    .slice(2)}-${type}-${Math.random()
+    .toString(16)
+    .slice(2)}`;
 }
 
 export const ADD_TOAST = 'ADD_TOAST';
diff --git a/superset/assets/src/dashboard/actions/sliceEntities.js b/superset/assets/src/dashboard/actions/sliceEntities.js
index 3a1b1dc..6922753 100644
--- a/superset/assets/src/dashboard/actions/sliceEntities.js
+++ b/superset/assets/src/dashboard/actions/sliceEntities.js
@@ -9,16 +9,15 @@ export function updateSliceName(key, sliceName) {
 
 export function saveSliceName(slice, sliceName) {
   const oldName = slice.slice_name;
-  return (dispatch) => {
+  return dispatch => {
     const sliceParams = {};
     sliceParams.slice_id = slice.slice_id;
     sliceParams.action = 'overwrite';
     sliceParams.slice_name = sliceName;
 
-    const url = slice.slice_url + '&' +
-      Object.keys(sliceParams)
-      .map(key => (key + '=' + sliceParams[key]))
-      .join('&');
+    const url = `${slice.slice_url}&${Object.keys(sliceParams)
+      .map(key => `${key}=${sliceParams[key]}`)
+      .join('&')}`;
     const key = slice.slice_id;
     return $.ajax({
       url,
@@ -54,7 +53,7 @@ export function fetchAllSlicesFailed(error) {
 
 export function fetchAllSlices(userId) {
   return (dispatch, getState) => {
-    const { sliceEntities }  = getState();
+    const { sliceEntities } = getState();
     if (sliceEntities.lastUpdated === 0) {
       dispatch(fetchAllSlicesStarted());
 
@@ -62,9 +61,9 @@ export function fetchAllSlices(userId) {
       return $.ajax({
         url: uri,
         type: 'GET',
-        success: (response) => {
+        success: response => {
           const slices = {};
-          response.result.forEach((slice) => {
+          response.result.forEach(slice => {
             const form_data = JSON.parse(slice.params);
             slices[slice.id] = {
               slice_id: slice.id,
diff --git a/superset/assets/src/dashboard/components/ActionMenuItem.jsx b/superset/assets/src/dashboard/components/ActionMenuItem.jsx
index aaae4df..a0ecb78 100644
--- a/superset/assets/src/dashboard/components/ActionMenuItem.jsx
+++ b/superset/assets/src/dashboard/components/ActionMenuItem.jsx
@@ -7,9 +7,7 @@ import InfoTooltipWithTrigger from '../../components/InfoTooltipWithTrigger';
 export function MenuItemContent({ faIcon, text, tooltip, children }) {
   return (
     <span>
-      {faIcon &&
-        <i className={`fa fa-${faIcon}`}>&nbsp;</i>
-      }
+      {faIcon && <i className={`fa fa-${faIcon}`}>&nbsp;</i>}
       {text} {''}
       <InfoTooltipWithTrigger
         tooltip={tooltip}
@@ -20,6 +18,7 @@ export function MenuItemContent({ faIcon, text, tooltip, children }) {
     </span>
   );
 }
+
 MenuItemContent.propTypes = {
   faIcon: PropTypes.string,
   text: PropTypes.string,
@@ -27,19 +26,40 @@ MenuItemContent.propTypes = {
   children: PropTypes.node,
 };
 
-export function ActionMenuItem(props) {
+MenuItemContent.defaultProps = {
+  faIcon: '',
+  text: '',
+  tooltip: null,
+  children: null,
+};
+
+export function ActionMenuItem({
+  onClick,
+  href,
+  target,
+  text,
+  tooltip,
+  children,
+  faIcon,
+}) {
   return (
-    <MenuItem
-      onClick={props.onClick}
-      href={props.href}
-      target={props.target}
-    >
-      <MenuItemContent {...props} />
+    <MenuItem onClick={onClick} href={href} target={target}>
+      <MenuItemContent faIcon={faIcon} text={text} tooltip={tooltip}>
+        {children}
+      </MenuItemContent>
     </MenuItem>
   );
 }
+
 ActionMenuItem.propTypes = {
   onClick: PropTypes.func,
   href: PropTypes.string,
   target: PropTypes.string,
+  ...MenuItemContent.propTypes,
+};
+
+ActionMenuItem.defaultProps = {
+  onClick() {},
+  href: null,
+  target: null,
 };
diff --git a/superset/assets/src/dashboard/components/AddSliceCard.jsx b/superset/assets/src/dashboard/components/AddSliceCard.jsx
new file mode 100644
index 0000000..7fd9ba4
--- /dev/null
+++ b/superset/assets/src/dashboard/components/AddSliceCard.jsx
@@ -0,0 +1,59 @@
+import cx from 'classnames';
+import React from 'react';
+import PropTypes from 'prop-types';
+
+const propTypes = {
+  datasourceLink: PropTypes.string,
+  innerRef: PropTypes.func,
+  isSelected: PropTypes.bool,
+  lastModified: PropTypes.string.isRequired,
+  sliceName: PropTypes.string.isRequired,
+  style: PropTypes.object,
+  visType: PropTypes.string.isRequired,
+};
+
+const defaultProps = {
+  datasourceLink: '—',
+  innerRef: null,
+  isSelected: false,
+  style: null,
+};
+
+function AddSliceCard({
+  datasourceLink,
+  innerRef,
+  isSelected,
+  lastModified,
+  sliceName,
+  style,
+  visType,
+}) {
+  return (
+    <div ref={innerRef} className="chart-card-container" style={style}>
+      <div className={cx('chart-card', isSelected && 'is-selected')}>
+        <div className="card-title">{sliceName}</div>
+        <div className="card-body">
+          <div className="item">
+            <span>Modified </span>
+            <span>{lastModified}</span>
+          </div>
+          <div className="item">
+            <span>Visualization </span>
+            <span>{visType}</span>
+          </div>
+          <div className="item">
+            <span>Data source </span>
+            <span // eslint-disable-next-line react/no-danger
+              dangerouslySetInnerHTML={{ __html: datasourceLink }}
+            />
+          </div>
+        </div>
+      </div>
+    </div>
+  );
+}
+
+AddSliceCard.propTypes = propTypes;
+AddSliceCard.defaultProps = defaultProps;
+
+export default AddSliceCard;
diff --git a/superset/assets/src/dashboard/v2/components/BuilderComponentPane.jsx b/superset/assets/src/dashboard/components/BuilderComponentPane.jsx
similarity index 85%
rename from superset/assets/src/dashboard/v2/components/BuilderComponentPane.jsx
rename to superset/assets/src/dashboard/components/BuilderComponentPane.jsx
index f9a37cc..e5bc74c 100644
--- a/superset/assets/src/dashboard/v2/components/BuilderComponentPane.jsx
+++ b/superset/assets/src/dashboard/components/BuilderComponentPane.jsx
@@ -6,9 +6,7 @@ import NewDivider from './gridComponents/new/NewDivider';
 import NewHeader from './gridComponents/new/NewHeader';
 import NewRow from './gridComponents/new/NewRow';
 import NewTabs from './gridComponents/new/NewTabs';
-import SliceAdderContainer from '../../../dashboard/components/SliceAdderContainer';
-
-import '../stylesheets/builder-sidepane.less';
+import SliceAdderContainer from '../containers/SliceAdder';
 
 class BuilderComponentPane extends React.PureComponent {
   constructor(props) {
@@ -32,9 +30,13 @@ class BuilderComponentPane extends React.PureComponent {
       <div className="dashboard-builder-sidepane">
         <div className="dashboard-builder-sidepane-header">
           Insert components
-          {this.state.showSlices &&
-            <i className="fa fa-times close trigger" onClick={this.closeSlicesPane} role="none" />
-          }
+          {this.state.showSlices && (
+            <i
+              className="fa fa-times close trigger"
+              onClick={this.closeSlicesPane}
+              role="none"
+            />
+          )}
         </div>
 
         <div className="component-layer">
@@ -52,7 +54,6 @@ class BuilderComponentPane extends React.PureComponent {
 
           <NewHeader />
           <NewDivider />
-
           <NewTabs />
           <NewRow />
           <NewColumn />
diff --git a/superset/assets/src/dashboard/components/CodeModal.jsx b/superset/assets/src/dashboard/components/CodeModal.jsx
index 0e84ad1..cc0c9f2 100644
--- a/superset/assets/src/dashboard/components/CodeModal.jsx
+++ b/superset/assets/src/dashboard/components/CodeModal.jsx
@@ -12,13 +12,16 @@ const propTypes = {
 
 const defaultProps = {
   codeCallback: () => {},
+  code: '',
 };
 
 export default class CodeModal extends React.PureComponent {
   constructor(props) {
     super(props);
     this.state = { code: props.code };
+    this.beforeOpen = this.beforeOpen.bind(this);
   }
+
   beforeOpen() {
     let code = this.props.code;
     if (!code && this.props.codeCallback) {
@@ -26,18 +29,17 @@ export default class CodeModal extends React.PureComponent {
     }
     this.setState({ code });
   }
+
   render() {
     return (
       <ModalTrigger
         triggerNode={this.props.triggerNode}
         isButton
-        beforeOpen={this.beforeOpen.bind(this)}
+        beforeOpen={this.beforeOpen}
         modalTitle={t('Active Dashboard Filters')}
         modalBody={
           <div className="CodeModal">
-            <pre>
-              {this.state.code}
-            </pre>
+            <pre>{this.state.code}</pre>
           </div>
         }
       />
diff --git a/superset/assets/src/dashboard/components/Controls.jsx b/superset/assets/src/dashboard/components/Controls.jsx
index 8755e8f..06b4f7f 100644
--- a/superset/assets/src/dashboard/components/Controls.jsx
+++ b/superset/assets/src/dashboard/components/Controls.jsx
@@ -1,3 +1,4 @@
+/* global window */
 import React from 'react';
 import PropTypes from 'prop-types';
 import $ from 'jquery';
@@ -8,6 +9,24 @@ import SaveModal from './SaveModal';
 import { ActionMenuItem, MenuItemContent } from './ActionMenuItem';
 import { t } from '../../locales';
 
+function updateDom(css) {
+  const className = 'CssEditor-css';
+  const head = document.head || document.getElementsByTagName('head')[0];
+  let style = document.querySelector(`.${className}`);
+
+  if (!style) {
+    style = document.createElement('style');
+    style.className = className;
+    style.type = 'text/css';
+    head.appendChild(style);
+  }
+  if (style.styleSheet) {
+    style.styleSheet.cssText = css;
+  } else {
+    style.innerHTML = css;
+  }
+}
+
 const propTypes = {
   dashboardInfo: PropTypes.object.isRequired,
   dashboardTitle: PropTypes.string.isRequired,
@@ -15,13 +34,18 @@ const propTypes = {
   filters: PropTypes.object.isRequired,
   expandedSlices: PropTypes.object.isRequired,
   slices: PropTypes.array,
-  onSave: PropTypes.func,
-  onChange: PropTypes.func,
-  forceRefreshAllCharts: PropTypes.func,
-  startPeriodicRender: PropTypes.func,
+  onSave: PropTypes.func.isRequired,
+  onChange: PropTypes.func.isRequired,
+  forceRefreshAllCharts: PropTypes.func.isRequired,
+  startPeriodicRender: PropTypes.func.isRequired,
   editMode: PropTypes.bool,
 };
 
+const defaultProps = {
+  editMode: false,
+  slices: [],
+};
+
 class Controls extends React.PureComponent {
   constructor(props) {
     super(props);
@@ -29,13 +53,12 @@ class Controls extends React.PureComponent {
       css: '',
       cssTemplates: [],
     };
-    this.toggleModal = this.toggleModal.bind(this);
-    this.updateDom = this.updateDom.bind(this);
   }
+
   componentWillMount() {
-    this.updateDom(this.state.css);
+    updateDom(this.state.css);
 
-    $.get('/csstemplateasyncmodelview/api/read', (data) => {
+    $.get('/csstemplateasyncmodelview/api/read', data => {
       const cssTemplates = data.result.map(row => ({
         value: row.template_name,
         css: row.css,
@@ -44,57 +67,48 @@ class Controls extends React.PureComponent {
       this.setState({ cssTemplates });
     });
   }
-  toggleModal(modal) {
-    let currentModal;
-    if (modal !== this.state.currentModal) {
-      currentModal = modal;
-    }
-    this.setState({ currentModal });
-  }
+
   changeCss(css) {
     this.setState({ css }, () => {
-      this.updateDom(css);
+      updateDom(css);
     });
     this.props.onChange();
   }
-  updateDom(css) {
-    const className = 'CssEditor-css';
-    const head = document.head || document.getElementsByTagName('head')[0];
-    let style = document.querySelector('.' + className);
 
-    if (!style) {
-      style = document.createElement('style');
-      style.className = className;
-      style.type = 'text/css';
-      head.appendChild(style);
-    }
-    if (style.styleSheet) {
-      style.styleSheet.cssText = css;
-    } else {
-      style.innerHTML = css;
-    }
-  }
   render() {
-    const { dashboardTitle, layout, filters, expandedSlices,
-      startPeriodicRender, forceRefreshAllCharts, onSave,
-      editMode } = this.props;
+    const {
+      dashboardTitle,
+      layout,
+      filters,
+      expandedSlices,
+      startPeriodicRender,
+      forceRefreshAllCharts,
+      onSave,
+      editMode,
+    } = this.props;
+
     const emailBody = t('Checkout this dashboard: %s', window.location.href);
-    const emailLink = 'mailto:?Subject=Superset%20Dashboard%20'
-      + `${dashboardTitle}&Body=${emailBody}`;
-    let saveText = t('Save as');
-    if (editMode) {
-      saveText = t('Save');
-    }
+    const emailLink =
+      'mailto:?Subject=Superset%20Dashboard%20' +
+      `${dashboardTitle}&Body=${emailBody}`;
+
     return (
       <span>
-        <DropdownButton title="Actions" bsSize="small" id="bg-nested-dropdown" pullRight>
+        <DropdownButton
+          title="Actions"
+          bsSize="small"
+          id="bg-nested-dropdown"
+          pullRight
+        >
           <ActionMenuItem
             text={t('Force Refresh')}
             tooltip={t('Force refresh the whole dashboard')}
             onClick={forceRefreshAllCharts}
           />
           <RefreshIntervalModal
-            onChange={refreshInterval => startPeriodicRender(refreshInterval * 1000)}
+            onChange={refreshInterval =>
+              startPeriodicRender(refreshInterval * 1000)
+            }
             triggerNode={
               <MenuItemContent
                 text={t('Set autorefresh')}
@@ -112,30 +126,39 @@ class Controls extends React.PureComponent {
             css={this.state.css}
             triggerNode={
               <MenuItemContent
-                text={saveText}
+                text={editMode ? t('Save') : t('Save as')}
                 tooltip={t('Save the dashboard')}
               />
             }
+            isMenuItem
           />
-          {editMode &&
+          {editMode && (
             <ActionMenuItem
               text={t('Edit properties')}
               tooltip={t("Edit the dashboards's properties")}
-              onClick={() => { window.location = `/dashboardmodelview/edit/${this.props.dashboardInfo.id}`; }}
+              onClick={() => {
+                window.location = `/dashboardmodelview/edit/${
+                  this.props.dashboardInfo.id
+                }`;
+              }}
             />
-          }
-          {editMode &&
+          )}
+          {editMode && (
             <ActionMenuItem
               text={t('Email')}
               tooltip={t('Email a link to this dashboard')}
-              onClick={() => { window.location = emailLink; }}
+              onClick={() => {
+                window.location = emailLink;
+              }}
             />
-          }
+          )}
         </DropdownButton>
       </span>
     );
   }
 }
+
 Controls.propTypes = propTypes;
+Controls.defaultProps = defaultProps;
 
 export default Controls;
diff --git a/superset/assets/src/dashboard/components/CssEditor.jsx b/superset/assets/src/dashboard/components/CssEditor.jsx
index 5abf5f8..45ef86d 100644
--- a/superset/assets/src/dashboard/components/CssEditor.jsx
+++ b/superset/assets/src/dashboard/components/CssEditor.jsx
@@ -29,15 +29,20 @@ class CssEditor extends React.PureComponent {
       css: props.initialCss,
       cssTemplateOptions: [],
     };
+    this.changeCss = this.changeCss.bind(this);
+    this.changeCssTemplate = this.changeCssTemplate.bind(this);
   }
+
   changeCss(css) {
     this.setState({ css }, () => {
       this.props.onChange(css);
     });
   }
+
   changeCssTemplate(opt) {
     this.changeCss(opt.css);
   }
+
   renderTemplateSelector() {
     if (this.props.templates) {
       return (
@@ -46,13 +51,14 @@ class CssEditor extends React.PureComponent {
           <Select
             options={this.props.templates}
             placeholder={t('Load a CSS template')}
-            onChange={this.changeCssTemplate.bind(this)}
+            onChange={this.changeCssTemplate}
           />
         </div>
       );
     }
     return null;
   }
+
   render() {
     return (
       <ModalTrigger
@@ -70,7 +76,7 @@ class CssEditor extends React.PureComponent {
                   theme="github"
                   minLines={8}
                   maxLines={30}
-                  onChange={this.changeCss.bind(this)}
+                  onChange={this.changeCss}
                   height="200px"
                   width="100%"
                   editorProps={{ $blockScrolling: true }}
@@ -85,6 +91,7 @@ class CssEditor extends React.PureComponent {
     );
   }
 }
+
 CssEditor.propTypes = propTypes;
 CssEditor.defaultProps = defaultProps;
 
diff --git a/superset/assets/src/dashboard/components/Dashboard.jsx b/superset/assets/src/dashboard/components/Dashboard.jsx
index 939476c..2d85ebf 100644
--- a/superset/assets/src/dashboard/components/Dashboard.jsx
+++ b/superset/assets/src/dashboard/components/Dashboard.jsx
@@ -1,26 +1,36 @@
+/* global window */
 import React from 'react';
 import PropTypes from 'prop-types';
 
 import AlertsWrapper from '../../components/AlertsWrapper';
-import GridLayout from './GridLayout';
+import getChartIdsFromLayout from '../util/getChartIdsFromLayout';
+import DashboardBuilder from '../containers/DashboardBuilder';
 import {
   chartPropShape,
   slicePropShape,
   dashboardInfoPropShape,
   dashboardStatePropShape,
-} from '../v2/util/propShapes';
-import { exportChart } from '../../explore/exploreUtils';
+} from '../util/propShapes';
 import { areObjectsEqual } from '../../reduxUtils';
-import { getChartIdsFromLayout } from '../util/dashboardHelper';
-import { Logger, ActionLog, LOG_ACTIONS_PAGE_LOAD,
-  LOG_ACTIONS_LOAD_EVENT, LOG_ACTIONS_RENDER_EVENT } from '../../logger';
+import getFormDataWithExtraFilters from '../util/charts/getFormDataWithExtraFilters';
+import {
+  Logger,
+  ActionLog,
+  LOG_ACTIONS_PAGE_LOAD,
+  LOG_ACTIONS_LOAD_EVENT,
+  LOG_ACTIONS_RENDER_EVENT,
+} from '../../logger';
 import { t } from '../../locales';
 
-import '../../../stylesheets/dashboard.less';
-import '../v2/stylesheets/index.less';
+import '../stylesheets/index.less';
 
 const propTypes = {
-  actions: PropTypes.object.isRequired,
+  actions: PropTypes.shape({
+    addSliceToDashboard: PropTypes.func.isRequired,
+    onChange: PropTypes.func.isRequired,
+    removeSliceFromDashboard: PropTypes.func.isRequired,
+    runQuery: PropTypes.func.isRequired,
+  }).isRequired,
   dashboardInfo: dashboardInfoPropShape.isRequired,
   dashboardState: dashboardStatePropShape.isRequired,
   charts: PropTypes.objectOf(chartPropShape).isRequired,
@@ -40,6 +50,20 @@ const defaultProps = {
 };
 
 class Dashboard extends React.PureComponent {
+  static onBeforeUnload(hasChanged) {
+    if (hasChanged) {
+      window.addEventListener('beforeunload', Dashboard.unload);
+    } else {
+      window.removeEventListener('beforeunload', Dashboard.unload);
+    }
+  }
+
+  static unload() {
+    const message = t('You have unsaved changes.');
+    window.event.returnValue = message; // Gecko + IE
+    return message; // Gecko + Webkit, Safari, Chrome etc.
+  }
+
   constructor(props) {
     super(props);
 
@@ -52,31 +76,15 @@ class Dashboard extends React.PureComponent {
       eventNames: [LOG_ACTIONS_LOAD_EVENT, LOG_ACTIONS_RENDER_EVENT],
     });
     Logger.start(this.loadingLog);
-
-    this.rerenderCharts = this.rerenderCharts.bind(this);
-    this.getFilters = this.getFilters.bind(this);
-    this.refreshExcept = this.refreshExcept.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);
-  }
-
-  componentDidMount() {
-    window.addEventListener('resize', this.rerenderCharts);
   }
 
   componentWillReceiveProps(nextProps) {
-    if (this.firstLoad &&
-      Object.values(nextProps.charts)
-        .every(chart => (['rendered', 'failed', 'stopped'].indexOf(chart.chartStatus) > -1))
+    if (
+      this.firstLoad &&
+      Object.values(nextProps.charts).every(
+        chart =>
+          ['rendered', 'failed', 'stopped'].indexOf(chart.chartStatus) > -1,
+      )
     ) {
       Logger.end(this.loadingLog);
       this.firstLoad = false;
@@ -86,13 +94,19 @@ class Dashboard extends React.PureComponent {
     const nextChartIds = getChartIdsFromLayout(nextProps.layout);
     if (currentChartIds.length < nextChartIds.length) {
       // adding new chart
-      const newChartId = nextChartIds.find(key => (currentChartIds.indexOf(key) === -1));
+      const newChartId = nextChartIds.find(
+        key => currentChartIds.indexOf(key) === -1,
+      );
       this.props.actions.addSliceToDashboard(newChartId);
       this.props.actions.onChange();
     } else if (currentChartIds.length > nextChartIds.length) {
       // remove chart
-      const removedChartId = currentChartIds.find(key => (nextChartIds.indexOf(key) === -1));
-      this.props.actions.removeSliceFromDashboard(this.props.charts[removedChartId]);
+      const removedChartId = currentChartIds.find(
+        key => nextChartIds.indexOf(key) === -1,
+      );
+      this.props.actions.removeSliceFromDashboard(
+        this.props.charts[removedChartId],
+      );
       this.props.actions.onChange();
     }
   }
@@ -101,11 +115,15 @@ class Dashboard extends React.PureComponent {
     const { refresh, filters, hasUnsavedChanges } = this.props.dashboardState;
     if (refresh) {
       let changedFilterKey;
-      const prevFiltersKeySet = new Set(Object.keys(prevProps.dashboardState.filters));
-      Object.keys(filters).some((key) => {
+      const prevFiltersKeySet = new Set(
+        Object.keys(prevProps.dashboardState.filters),
+      );
+      Object.keys(filters).some(key => {
         prevFiltersKeySet.delete(key);
-        if (prevProps.dashboardState.filters[key] === undefined ||
-          !areObjectsEqual(prevProps.dashboardState.filters[key], filters[key])) {
+        if (
+          prevProps.dashboardState.filters[key] === undefined ||
+          !areObjectsEqual(prevProps.dashboardState.filters[key], filters[key])
+        ) {
           changedFilterKey = key;
           return true;
         }
@@ -118,21 +136,9 @@ class Dashboard extends React.PureComponent {
     }
 
     if (hasUnsavedChanges) {
-      this.onBeforeUnload(true);
-    } else {
-      this.onBeforeUnload(false);
-    }
-  }
-
-  componentWillUnmount() {
-    window.removeEventListener('resize', this.rerenderCharts);
-  }
-
-  onBeforeUnload(hasChanged) {
-    if (hasChanged) {
-      window.addEventListener('beforeunload', this.unload);
+      Dashboard.onBeforeUnload(true);
     } else {
-      window.removeEventListener('beforeunload', this.unload);
+      Dashboard.onBeforeUnload(false);
     }
   }
 
@@ -141,131 +147,37 @@ class Dashboard extends React.PureComponent {
     return Object.values(this.props.charts);
   }
 
-  getFormDataExtra(chart) {
-    const extraFilters = this.effectiveExtraFilters(chart.id);
-    const formDataExtra = {
-      ...chart.formData,
-      extra_filters: extraFilters,
-    };
-    return formDataExtra;
-  }
-
-  getFilters(sliceId) {
-    return this.props.dashboardState.filters[sliceId];
-  }
-
-  unload() {
-    const message = t('You have unsaved changes.');
-    window.event.returnValue = message; // Gecko + IE
-    return message; // Gecko + Webkit, Safari, Chrome etc.
-  }
-
-  effectiveExtraFilters(sliceId) {
-    const metadata = this.props.dashboardInfo.metadata;
-    const filters = this.props.dashboardState.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.dashboardInfo.metadata.filter_immune_slices || [];
     let charts = this.getAllCharts();
     if (filterKey) {
       charts = charts.filter(
-        chart => (String(chart.id) !== filterKey && immune.indexOf(chart.id) === -1),
+        chart =>
+          String(chart.id) !== filterKey && immune.indexOf(chart.id) === -1,
       );
     }
-    charts.forEach((chart) => {
-      const updatedFormData = this.getFormDataExtra(chart);
-      this.props.actions.runQuery(updatedFormData, false, this.props.timeout, chart.id);
-    });
-  }
-
-  exploreChart(chartId) {
-    const chart = this.props.charts[chartId];
-    const formData = this.getFormDataExtra(chart);
-    exportChart(formData);
-  }
-
-  exportCSV(chartId) {
-    const chart = this.props.charts[chartId];
-    const formData = this.getFormDataExtra(chart);
-    exportChart(formData, 'csv');
-  }
+    charts.forEach(chart => {
+      const updatedFormData = getFormDataWithExtraFilters({
+        chart,
+        dashboardMetadata: this.props.dashboardInfo.metadata,
+        filters: this.props.dashboardState.filters,
+        sliceId: chart.id,
+      });
 
-  // re-render chart without fetch
-  rerenderCharts() {
-    this.getAllCharts().forEach((chart) => {
-      setTimeout(() => {
-        this.props.actions.renderTriggered(new Date().getTime(), chart.id);
-      }, 50);
+      this.props.actions.runQuery(
+        updatedFormData,
+        false,
+        this.props.timeout,
+        chart.id,
+      );
     });
   }
 
   render() {
-    const {
-      expandedSlices = {}, filters, sliceIds,
-      editMode, showBuilderPane,
-    } = this.props.dashboardState;
-
     return (
-      <div id="dashboard-container">
-        <div>
-          <AlertsWrapper initMessages={this.props.initMessages} />
-        </div>
-        <GridLayout
-          dashboardInfo={this.props.dashboardInfo}
-          layout={this.props.layout}
-          datasources={this.props.datasources}
-          slices={this.props.slices}
-          sliceIds={sliceIds}
-          expandedSlices={expandedSlices}
-          filters={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={editMode}
-          showBuilderPane={showBuilderPane}
-        />
+      <div>
+        <AlertsWrapper initMessages={this.props.initMessages} />
+        <DashboardBuilder />
       </div>
     );
   }
diff --git a/superset/assets/src/dashboard/v2/components/DashboardBuilder.jsx b/superset/assets/src/dashboard/components/DashboardBuilder.jsx
similarity index 81%
rename from superset/assets/src/dashboard/v2/components/DashboardBuilder.jsx
rename to superset/assets/src/dashboard/components/DashboardBuilder.jsx
index f3f5867..79eb35d 100644
--- a/superset/assets/src/dashboard/v2/components/DashboardBuilder.jsx
+++ b/superset/assets/src/dashboard/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,
@@ -37,8 +35,10 @@ const defaultProps = {
 class DashboardBuilder extends React.Component {
   static shouldFocusTabs(event, container) {
     // don't focus the tabs when we click on a tab
-    return event.target.tagName === 'UL' || (
-      /icon-button/.test(event.target.className) && container.contains(event.target)
+    return (
+      event.target.tagName === 'UL' ||
+      (/icon-button/.test(event.target.className) &&
+        container.contains(event.target))
     );
   }
 
@@ -56,19 +56,27 @@ class DashboardBuilder extends React.Component {
 
   render() {
     const { tabIndex } = this.state;
-    const { handleComponentDrop, dashboardLayout, deleteTopLevelTabs, editMode } = this.props;
+    const {
+      handleComponentDrop,
+      dashboardLayout,
+      deleteTopLevelTabs,
+      editMode,
+    } = this.props;
     const dashboardRoot = dashboardLayout[DASHBOARD_ROOT_ID];
     const rootChildId = dashboardRoot.children[0];
-    const topLevelTabs = rootChildId !== DASHBOARD_GRID_ID && dashboardLayout[rootChildId];
+    const topLevelTabs =
+      rootChildId !== DASHBOARD_GRID_ID && dashboardLayout[rootChildId];
 
     const gridComponentId = topLevelTabs
-      ? topLevelTabs.children[Math.min(topLevelTabs.children.length - 1, tabIndex)]
+      ? topLevelTabs.children[
+          Math.min(topLevelTabs.children.length - 1, tabIndex)
+        ]
       : DASHBOARD_GRID_ID;
 
     const gridComponent = dashboardLayout[gridComponentId];
 
     return (
-      <div className={cx('dashboard-v2', editMode && 'dashboard-v2--editing')}>
+      <div className={cx('dashboard', editMode && 'dashboard--editing')}>
         {topLevelTabs || !editMode ? ( // you cannot drop on/displace tabs if they already exist
           <DashboardHeader />
         ) : (
@@ -87,9 +95,10 @@ class DashboardBuilder extends React.Component {
                 {dropIndicatorProps && <div {...dropIndicatorProps} />}
               </div>
             )}
-          </DragDroppable>)}
+          </DragDroppable>
+        )}
 
-        {topLevelTabs &&
+        {topLevelTabs && (
           <WithPopoverMenu
             shouldFocus={DashboardBuilder.shouldFocusTabs}
             menuItems={[
@@ -108,19 +117,17 @@ class DashboardBuilder extends React.Component {
               index={0}
               renderTabContent={false}
               onChangeTab={this.handleChangeTab}
-              cells={this.props.cells}
             />
-          </WithPopoverMenu>}
+          </WithPopoverMenu>
+        )}
 
         <div className="dashboard-content">
           <DashboardGrid
             gridComponent={gridComponent}
             depth={DASHBOARD_ROOT_DEPTH + 1}
-            cells={this.props.cells}
           />
-          {this.props.editMode && this.props.showBuilderPane &&
-            <BuilderComponentPane />
-          }
+          {this.props.editMode &&
+            this.props.showBuilderPane && <BuilderComponentPane />}
         </div>
         <ToastPresenter />
       </div>
diff --git a/superset/assets/src/dashboard/components/DashboardContainer.jsx b/superset/assets/src/dashboard/components/DashboardContainer.jsx
deleted file mode 100644
index 31fe035..0000000
--- a/superset/assets/src/dashboard/components/DashboardContainer.jsx
+++ /dev/null
@@ -1,51 +0,0 @@
-import { bindActionCreators } from 'redux';
-import { connect } from 'react-redux';
-
-import {
-  toggleExpandSlice,
-  addFilter,
-  removeFilter,
-  addSliceToDashboard,
-  removeSliceFromDashboard,
-  onChange,
-} from '../actions/dashboardState';
-import { saveSliceName } from '../actions/sliceEntities';
-import { refreshChart, runQuery, renderTriggered } from '../../chart/chartAction';
-import Dashboard from './Dashboard';
-
-function mapStateToProps({ datasources, sliceEntities, charts,
-                           dashboardInfo, dashboardState,
-                           dashboardLayout, impressionId }) {
-  return {
-    initMessages: dashboardInfo.common.flash_messages,
-    timeout: dashboardInfo.common.conf.SUPERSET_WEBSERVER_TIMEOUT,
-    userId: dashboardInfo.userId,
-    dashboardInfo,
-    dashboardState,
-    charts,
-    datasources,
-    slices: sliceEntities.slices,
-    layout: dashboardLayout.present,
-    impressionId,
-  };
-}
-
-function mapDispatchToProps(dispatch) {
-  const actions = {
-    refreshChart,
-    runQuery,
-    renderTriggered,
-    saveSliceName,
-    toggleExpandSlice,
-    addFilter,
-    removeFilter,
-    addSliceToDashboard,
-    removeSliceFromDashboard,
-    onChange,
-  };
-  return {
-    actions: bindActionCreators(actions, dispatch),
-  };
-}
-
-export default connect(mapStateToProps, mapDispatchToProps)(Dashboard);
diff --git a/superset/assets/src/dashboard/v2/components/DashboardGrid.jsx b/superset/assets/src/dashboard/components/DashboardGrid.jsx
similarity index 57%
rename from superset/assets/src/dashboard/v2/components/DashboardGrid.jsx
rename to superset/assets/src/dashboard/components/DashboardGrid.jsx
index 2aa82af..3e6fc0c 100644
--- a/superset/assets/src/dashboard/v2/components/DashboardGrid.jsx
+++ b/superset/assets/src/dashboard/components/DashboardGrid.jsx
@@ -1,15 +1,14 @@
 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';
 import DashboardComponent from '../containers/DashboardComponent';
 import DragDroppable from './dnd/DragDroppable';
 
-import {
-  GRID_GUTTER_SIZE,
-  GRID_COLUMN_COUNT,
-} from '../util/constants';
+import { GRID_GUTTER_SIZE, GRID_COLUMN_COUNT } from '../util/constants';
 
 const propTypes = {
   depth: PropTypes.number.isRequired,
@@ -19,8 +18,7 @@ const propTypes = {
   resizeComponent: PropTypes.func.isRequired,
 };
 
-const defaultProps = {
-};
+const defaultProps = {};
 
 class DashboardGrid extends React.PureComponent {
   constructor(props) {
@@ -34,15 +32,24 @@ 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) {
     if (resizeRef && this.grid) {
-      return resizeRef.getBoundingClientRect().bottom - this.grid.getBoundingClientRect().top - 1;
+      return (
+        resizeRef.getBoundingClientRect().bottom -
+        this.grid.getBoundingClientRect().top -
+        1
+      );
     }
     return null;
   }
 
+  setGridRef(ref) {
+    this.grid = ref;
+  }
+
   handleResizeStart({ ref, direction }) {
     let rowGuideTop = null;
     if (direction === 'bottom' || direction === 'bottomRight') {
@@ -71,19 +78,36 @@ 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
-            const columnPlusGutterWidth = (width + GRID_GUTTER_SIZE) / GRID_COLUMN_COUNT;
+            const columnPlusGutterWidth =
+              (width + GRID_GUTTER_SIZE) / GRID_COLUMN_COUNT;
             const columnWidth = columnPlusGutterWidth - GRID_GUTTER_SIZE;
-
             return width < 50 ? null : (
               <div className="grid-content">
+                {editMode && (
+                  <DragDroppable
+                    component={gridComponent}
+                    depth={depth}
+                    parentComponent={null}
+                    index={0}
+                    orientation="column"
+                    onDrop={handleComponentDrop}
+                    editMode
+                  >
+                    {({ dropIndicatorProps }) =>
+                      dropIndicatorProps && (
+                        <div className="drop-indicator drop-indicator--bottom" />
+                      )
+                    }
+                  </DragDroppable>
+                )}
+
                 {gridComponent.children.map((id, index) => (
                   <DashboardComponent
                     key={id}
@@ -93,7 +117,6 @@ class DashboardGrid extends React.PureComponent {
                     index={index}
                     availableColumnCount={GRID_COLUMN_COUNT}
                     columnWidth={columnWidth}
-                    cells={cells}
                     onResizeStart={this.handleResizeStart}
                     onResize={this.handleResize}
                     onResizeStop={this.handleResizeStop}
@@ -101,7 +124,7 @@ class DashboardGrid extends React.PureComponent {
                 ))}
 
                 {/* render an empty drop target */}
-                {editMode &&
+                {editMode && (
                   <DragDroppable
                     component={gridComponent}
                     depth={depth}
@@ -112,29 +135,38 @@ class DashboardGrid extends React.PureComponent {
                     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,
-                    }}
-                  />}
+                    {({ 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>
             );
           }}
diff --git a/superset/assets/src/dashboard/v2/components/DeleteComponentButton.jsx b/superset/assets/src/dashboard/components/DeleteComponentButton.jsx
similarity index 78%
rename from superset/assets/src/dashboard/v2/components/DeleteComponentButton.jsx
rename to superset/assets/src/dashboard/components/DeleteComponentButton.jsx
index 18efff4..8470947 100644
--- a/superset/assets/src/dashboard/v2/components/DeleteComponentButton.jsx
+++ b/superset/assets/src/dashboard/components/DeleteComponentButton.jsx
@@ -7,15 +7,12 @@ const propTypes = {
   onDelete: PropTypes.func.isRequired,
 };
 
-const defaultProps = {
-};
+const defaultProps = {};
 
 export default class DeleteComponentButton extends React.PureComponent {
   render() {
     const { onDelete } = this.props;
-    return (
-      <IconButton onClick={onDelete} className="fa fa-trash" />
-    );
+    return <IconButton onClick={onDelete} className="fa fa-trash" />;
   }
 }
 
diff --git a/superset/assets/src/dashboard/components/GridCell.jsx b/superset/assets/src/dashboard/components/GridCell.jsx
deleted file mode 100644
index 3273272..0000000
--- a/superset/assets/src/dashboard/components/GridCell.jsx
+++ /dev/null
@@ -1,158 +0,0 @@
-/* eslint-disable react/no-danger */
-import React from 'react';
-import PropTypes from 'prop-types';
-
-import SliceHeader from './SliceHeader';
-import ChartContainer from '../../chart/ChartContainer';
-import { chartPropShape, slicePropShape } from '../v2/util/propShapes';
-
-const propTypes = {
-  timeout: PropTypes.number,
-  datasource: PropTypes.object,
-  isLoading: PropTypes.bool,
-  isCached: PropTypes.bool,
-  cachedDttm: PropTypes.string,
-  isExpanded: PropTypes.bool,
-  widgetHeight: PropTypes.number,
-  widgetWidth: PropTypes.number,
-  slice: slicePropShape.isRequired,
-  chart: chartPropShape.isRequired,
-  formData: PropTypes.object,
-  filters: PropTypes.object,
-  refreshChart: PropTypes.func,
-  updateSliceName: PropTypes.func,
-  toggleExpandSlice: PropTypes.func,
-  exploreChart: PropTypes.func,
-  exportCSV: PropTypes.func,
-  addFilter: PropTypes.func,
-  getFilters: PropTypes.func,
-  removeFilter: PropTypes.func,
-  editMode: PropTypes.bool,
-  annotationQuery: PropTypes.object,
-};
-
-const defaultProps = {
-  refreshChart: () => ({}),
-  updateSliceName: () => ({}),
-  toggleExpandSlice: () => ({}),
-  exploreChart: () => ({}),
-  exportCSV: () => ({}),
-  addFilter: () => ({}),
-  getFilters: () => ({}),
-  removeFilter: () => ({}),
-  editMode: false,
-};
-
-class GridCell extends React.PureComponent {
-  constructor(props) {
-    super(props);
-
-    const sliceId = this.props.slice.slice_id;
-    this.forceRefresh = this.forceRefresh.bind(this);
-    this.addFilter = this.props.addFilter.bind(this, this.props.chart);
-    this.getFilters = this.props.getFilters.bind(this, sliceId);
-    this.removeFilter = this.props.removeFilter.bind(this, sliceId);
-  }
-
-  getDescriptionId(slice) {
-    return 'description_' + slice.slice_id;
-  }
-
-  getHeaderId(slice) {
-    return 'header_' + slice.slice_id;
-  }
-
-  width() {
-    return this.props.widgetWidth - 32;
-  }
-
-  height(slice) {
-    const widgetHeight = this.props.widgetHeight;
-    const headerHeight = this.headerHeight(slice);
-    const descriptionId = this.getDescriptionId(slice);
-    let descriptionHeight = 0;
-    if (this.props.isExpanded && this.refs[descriptionId]) {
-      descriptionHeight = this.refs[descriptionId].offsetHeight + 10;
-    }
-
-    return widgetHeight - headerHeight - descriptionHeight - 32;
-  }
-
-  headerHeight(slice) {
-    const headerId = this.getHeaderId(slice);
-    return this.refs[headerId] ? this.refs[headerId].offsetHeight : 30;
-  }
-
-  forceRefresh() {
-    return this.props.refreshChart(this.props.chart, true, this.props.timeout);
-  }
-
-  render() {
-    const {
-      isExpanded, isLoading, isCached, cachedDttm,
-      updateSliceName, toggleExpandSlice,
-      chart, slice, datasource, formData, timeout, annotationQuery,
-      exploreChart, exportCSV, editMode,
-    } = this.props;
-
-    return (
-      <div
-        className={isLoading ? 'slice-cell-highlight' : 'slice-cell'}
-        id={`${slice.slice_id}-cell`}
-      >
-        <div ref={this.getHeaderId(slice)}>
-          <SliceHeader
-            slice={slice}
-            isExpanded={isExpanded}
-            isCached={isCached}
-            cachedDttm={cachedDttm}
-            updateSliceName={updateSliceName}
-            toggleExpandSlice={toggleExpandSlice}
-            forceRefresh={this.forceRefresh}
-            editMode={editMode}
-            annotationQuery={annotationQuery}
-            exploreChart={exploreChart}
-            exportCSV={exportCSV}
-          />
-        </div>
-        {
-        /* 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 ? {} : { display: 'none' }}
-          ref={this.getDescriptionId(slice)}
-          dangerouslySetInnerHTML={{ __html: slice.description_markeddown }}
-        />
-        <div
-          className="chart-container"
-          style={{ width: this.width(), height: this.height(slice) }}
-        >
-          <input type="hidden" value="false" />
-          <ChartContainer
-            containerId={`slice-container-${slice.slice_id}`}
-            chartId={chart.id}
-            datasource={datasource}
-            formData={formData}
-            headerHeight={this.headerHeight(slice)}
-            height={this.height(slice)}
-            width={this.width()}
-            timeout={timeout}
-            vizType={slice.viz_type}
-            addFilter={this.addFilter}
-            getFilters={this.getFilters}
-            removeFilter={this.removeFilter}
-          />
-        </div>
-      </div>
-    );
-  }
-}
-
-GridCell.propTypes = propTypes;
-GridCell.defaultProps = defaultProps;
-
-export default GridCell;
diff --git a/superset/assets/src/dashboard/components/GridLayout.jsx b/superset/assets/src/dashboard/components/GridLayout.jsx
deleted file mode 100644
index fd561e2..0000000
--- a/superset/assets/src/dashboard/components/GridLayout.jsx
+++ /dev/null
@@ -1,156 +0,0 @@
-import React from 'react';
-import PropTypes from 'prop-types';
-import cx from 'classnames';
-
-import GridCell from './GridCell';
-import { slicePropShape, chartPropShape } from '../v2/util/propShapes';
-import DashboardBuilder from '../v2/containers/DashboardBuilder';
-
-const propTypes = {
-  dashboardInfo: PropTypes.shape().isRequired,
-  layout: PropTypes.object.isRequired,
-  datasources: PropTypes.object,
-  charts: PropTypes.objectOf(chartPropShape).isRequired,
-  slices: PropTypes.objectOf(slicePropShape).isRequired,
-  expandedSlices: PropTypes.object.isRequired,
-  sliceIds: PropTypes.object.isRequired,
-  filters: PropTypes.object,
-  timeout: PropTypes.number,
-  onChange: PropTypes.func,
-  rerenderCharts: PropTypes.func,
-  getFormDataExtra: PropTypes.func,
-  exploreChart: PropTypes.func,
-  exportCSV: PropTypes.func,
-  refreshChart: PropTypes.func,
-  saveSliceName: PropTypes.func,
-  toggleExpandSlice: PropTypes.func,
-  addFilter: PropTypes.func,
-  getFilters: PropTypes.func,
-  removeFilter: PropTypes.func,
-  editMode: PropTypes.bool.isRequired,
-  showBuilderPane: PropTypes.bool.isRequired,
-};
-
-const defaultProps = {
-  expandedSlices: {},
-  filters: {},
-  timeout: 60,
-  onChange: () => ({}),
-  getFormDataExtra: () => ({}),
-  exploreChart: () => ({}),
-  exportCSV: () => ({}),
-  refreshChart: () => ({}),
-  saveSliceName: () => ({}),
-  toggleExpandSlice: () => ({}),
-  addFilter: () => ({}),
-  getFilters: () => ({}),
-  removeFilter: () => ({}),
-};
-
-class GridLayout extends React.Component {
-  constructor(props) {
-    super(props);
-
-    this.updateSliceName = this.props.dashboardInfo.dash_edit_perm ?
-      this.updateSliceName.bind(this) : null;
-  }
-
-  componentDidUpdate(prevProps) {
-    if (prevProps.editMode !== this.props.editMode ||
-      prevProps.showBuilderPane !== this.props.showBuilderPane) {
-      this.props.rerenderCharts();
-    }
-  }
-
-  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;
-  }
-
-  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 = sliceId;
-    const currentSlice = this.props.slices[key];
-    if (!currentSlice || currentSlice.slice_name === sliceName) {
-      return;
-    }
-
-    this.props.saveSliceName(currentSlice, sliceName);
-  }
-
-  isExpanded(sliceId) {
-    return this.props.expandedSlices[sliceId];
-  }
-
-  render() {
-    const cells = {};
-    this.props.sliceIds.forEach((sliceId) => {
-      const key = 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}
-      />
-    );
-  }
-}
-
-GridLayout.propTypes = propTypes;
-GridLayout.defaultProps = defaultProps;
-
-export default GridLayout;
diff --git a/superset/assets/src/dashboard/components/Header.jsx b/superset/assets/src/dashboard/components/Header.jsx
index 11c8991..242102e 100644
--- a/superset/assets/src/dashboard/components/Header.jsx
+++ b/superset/assets/src/dashboard/components/Header.jsx
@@ -6,8 +6,9 @@ import Controls from './Controls';
 import EditableTitle from '../../components/EditableTitle';
 import Button from '../../components/Button';
 import FaveStar from '../../components/FaveStar';
-import InfoTooltipWithTrigger from '../../components/InfoTooltipWithTrigger';
-import { chartPropShape } from '../v2/util/propShapes';
+// import InfoTooltipWithTrigger from '../../components/InfoTooltipWithTrigger';
+import SaveModal from './SaveModal';
+import { chartPropShape } from '../util/propShapes';
 import { t } from '../../locales';
 
 const propTypes = {
@@ -20,9 +21,9 @@ const propTypes = {
   isStarred: PropTypes.bool.isRequired,
   onSave: PropTypes.func.isRequired,
   onChange: PropTypes.func.isRequired,
-  fetchFaveStar: PropTypes.func,
+  fetchFaveStar: PropTypes.func.isRequired,
   fetchCharts: PropTypes.func.isRequired,
-  saveFaveStar: PropTypes.func,
+  saveFaveStar: PropTypes.func.isRequired,
   startPeriodicRender: PropTypes.func.isRequired,
   updateDashboardTitle: PropTypes.func.isRequired,
   editMode: PropTypes.bool.isRequired,
@@ -41,13 +42,16 @@ const propTypes = {
 class Header extends React.PureComponent {
   constructor(props) {
     super(props);
+
     this.handleChangeText = this.handleChangeText.bind(this);
     this.toggleEditMode = this.toggleEditMode.bind(this);
     this.forceRefresh = this.forceRefresh.bind(this);
   }
+
   forceRefresh() {
     return this.props.fetchCharts(Object.values(this.props.charts), true);
   }
+
   handleChangeText(nextText) {
     const { updateDashboardTitle, onChange } = this.props;
     if (nextText && this.props.dashboardTitle !== nextText) {
@@ -55,56 +59,31 @@ class Header extends React.PureComponent {
       onChange();
     }
   }
+
   toggleEditMode() {
     this.props.setEditMode(!this.props.editMode);
   }
-  renderUnsaved() {
-    if (!this.props.hasUnsavedChanges) {
-      return null;
-    }
-    return (
-      <InfoTooltipWithTrigger
-        label="unsaved"
-        tooltip={t('Unsaved changes')}
-        icon="exclamation-triangle"
-        className="text-danger m-r-5"
-        placement="top"
-      />
-    );
-  }
-  renderInsertButton() {
-    if (!this.props.editMode) {
-      return null;
-    }
-    const btnText = this.props.showBuilderPane ? t('Hide builder pane') : t('Insert components');
-    return (
-      <Button
-        bsSize="small"
-        onClick={this.props.toggleBuilderPane}
-      >
-        {btnText}
-      </Button>);
-  }
-  renderEditButton() {
-    if (!this.props.dashboardInfo.dash_save_perm) {
-      return null;
-    }
-    const btnText = this.props.editMode ? t('Switch to View Mode') : t('Edit Dashboard');
-    return (
-      <Button
-        bsSize="small"
-        onClick={this.toggleEditMode}
-      >
-        {btnText}
-      </Button>);
-  }
+
   render() {
     const {
-      dashboardTitle, layout, filters, expandedSlices,
-      onUndo, onRedo, canUndo, canRedo,
-      onChange, onSave, editMode,
+      dashboardTitle,
+      layout,
+      filters,
+      expandedSlices,
+      onUndo,
+      onRedo,
+      canUndo,
+      canRedo,
+      onChange,
+      onSave,
+      editMode,
+      showBuilderPane,
+      dashboardInfo,
+      hasUnsavedChanges,
     } = this.props;
 
+    const userCanEdit = dashboardInfo.dash_save_perm;
+
     return (
       <div className="dashboard-header">
         <div className="dashboard-component-header header-large">
@@ -122,27 +101,58 @@ class Header extends React.PureComponent {
               isStarred={this.props.isStarred}
             />
           </span>
-          {this.renderUnsaved()}
         </div>
         <ButtonToolbar>
-          <ButtonGroup>
-            <Button
-              bsSize="small"
-              onClick={onUndo}
-              disabled={!canUndo}
-            >
-              Undo
-            </Button>
-            <Button
-              bsSize="small"
-              onClick={onRedo}
-              disabled={!canRedo}
-            >
-              Redo
-            </Button>
-            {this.renderInsertButton()}
-            {this.renderEditButton()}
-          </ButtonGroup>
+          {userCanEdit && (
+            <ButtonGroup>
+              {editMode && (
+                <Button bsSize="small" onClick={onUndo} disabled={!canUndo}>
+                  Undo
+                </Button>
+              )}
+
+              {editMode && (
+                <Button bsSize="small" onClick={onRedo} disabled={!canRedo}>
+                  Redo
+                </Button>
+              )}
+
+              {editMode && (
+                <Button bsSize="small" onClick={this.props.toggleBuilderPane}>
+                  {showBuilderPane
+                    ? t('Hide builder pane')
+                    : t('Insert components')}
+                </Button>
+              )}
+
+              {!hasUnsavedChanges ? (
+                <Button
+                  bsSize="small"
+                  onClick={this.toggleEditMode}
+                  bsStyle={editMode ? undefined : 'primary'}
+                >
+                  {editMode ? t('Switch to View Mode') : t('Edit Dashboard')}
+                </Button>
+              ) : (
+                <SaveModal
+                  dashboardId={this.props.dashboardInfo.id}
+                  dashboardTitle={dashboardTitle}
+                  layout={layout}
+                  filters={filters}
+                  expandedSlices={expandedSlices}
+                  onSave={onSave}
+                  // @TODO need to figure out css
+                  css=""
+                  triggerNode={
+                    <Button bsStyle="primary" bsSize="small">
+                      {t('Save changes')}
+                    </Button>
+                  }
+                />
+              )}
+            </ButtonGroup>
+          )}
+
           <Controls
             dashboardInfo={this.props.dashboardInfo}
             dashboardTitle={dashboardTitle}
@@ -160,6 +170,7 @@ class Header extends React.PureComponent {
     );
   }
 }
+
 Header.propTypes = propTypes;
 
 export default Header;
diff --git a/superset/assets/src/dashboard/v2/components/IconButton.jsx b/superset/assets/src/dashboard/components/IconButton.jsx
similarity index 100%
rename from superset/assets/src/dashboard/v2/components/IconButton.jsx
rename to superset/assets/src/dashboard/components/IconButton.jsx
diff --git a/superset/assets/src/dashboard/components/RefreshIntervalModal.jsx b/superset/assets/src/dashboard/components/RefreshIntervalModal.jsx
index 2737a42..3d92dd5 100644
--- a/superset/assets/src/dashboard/components/RefreshIntervalModal.jsx
+++ b/superset/assets/src/dashboard/components/RefreshIntervalModal.jsx
@@ -16,7 +16,7 @@ const defaultProps = {
 };
 
 const options = [
-  [0, t('Don\'t refresh')],
+  [0, t("Don't refresh")],
   [10, t('10 seconds')],
   [30, t('30 seconds')],
   [60, t('1 minute')],
@@ -42,7 +42,7 @@ class RefreshIntervalModal extends React.PureComponent {
             <Select
               options={options}
               value={this.state.refreshFrequency}
-              onChange={(opt) => {
+              onChange={opt => {
                 const value = opt ? opt.value : options[0].value;
                 this.setState({
                   refreshFrequency: value,
diff --git a/superset/assets/src/dashboard/components/SaveModal.jsx b/superset/assets/src/dashboard/components/SaveModal.jsx
index 6a69361..07b904b 100644
--- a/superset/assets/src/dashboard/components/SaveModal.jsx
+++ b/superset/assets/src/dashboard/components/SaveModal.jsx
@@ -1,4 +1,4 @@
-/* global notify */
+/* global notify, window */
 import React from 'react';
 import PropTypes from 'prop-types';
 import $ from 'jquery';
@@ -17,6 +17,11 @@ const propTypes = {
   triggerNode: PropTypes.node.isRequired,
   filters: PropTypes.object.isRequired,
   onSave: PropTypes.func.isRequired,
+  isMenuItem: PropTypes.bool,
+};
+
+const defaultProps = {
+  isMenuItem: false,
 };
 
 class SaveModal extends React.PureComponent {
@@ -24,28 +29,38 @@ class SaveModal extends React.PureComponent {
     super(props);
     this.state = {
       saveType: 'overwrite',
-      newDashName: props.dashboardTitle + ' [copy]',
+      newDashName: `${props.dashboardTitle} [copy]`,
       duplicateSlices: false,
     };
     this.modal = null;
     this.handleSaveTypeChange = this.handleSaveTypeChange.bind(this);
     this.handleNameChange = this.handleNameChange.bind(this);
     this.saveDashboard = this.saveDashboard.bind(this);
+    this.setModalRef = this.setModalRef.bind(this);
+    this.toggleDuplicateSlices = this.toggleDuplicateSlices.bind(this);
+  }
+
+  setModalRef(ref) {
+    this.modal = ref;
   }
+
   toggleDuplicateSlices() {
     this.setState({ duplicateSlices: !this.state.duplicateSlices });
   }
+
   handleSaveTypeChange(event) {
     this.setState({
       saveType: event.target.value,
     });
   }
+
   handleNameChange(event) {
     this.setState({
       newDashName: event.target.value,
       saveType: 'newDashboard',
     });
   }
+
   saveDashboardRequest(data, url, saveType) {
     const saveModal = this.modal;
     const onSaveDashboard = this.props.onSave;
@@ -67,12 +82,25 @@ class SaveModal extends React.PureComponent {
       error(error) {
         saveModal.close();
         const errorMsg = getAjaxErrorMsg(error);
-        notify.error(t('Sorry, there was an error saving this dashboard: ') + '</ br>' + errorMsg);
+        notify.error(
+          `${t(
+            'Sorry, there was an error saving this dashboard: ',
+          )}</ br>${errorMsg}`,
+        );
       },
     });
   }
-  saveDashboard(saveType, newDashboardTitle) {
-    const { dashboardTitle, layout: positions, expandedSlices, filters, dashboardId } = this.props;
+
+  saveDashboard() {
+    const { saveType, newDashName } = this.state;
+    const {
+      dashboardTitle,
+      layout: positions,
+      expandedSlices,
+      filters,
+      dashboardId,
+    } = this.props;
+
     const data = {
       positions,
       expanded_slices: expandedSlices,
@@ -80,29 +108,27 @@ class SaveModal extends React.PureComponent {
       default_filters: JSON.stringify(filters),
       duplicate_slices: this.state.duplicateSlices,
     };
+
     let url = null;
     if (saveType === 'overwrite') {
       url = `/superset/save_dash/${dashboardId}/`;
       this.saveDashboardRequest(data, url, saveType);
     } else if (saveType === 'newDashboard') {
-      if (!newDashboardTitle) {
-        this.modal.close();
-        showModal({
-          title: t('Error'),
-          body: t('You must pick a name for the new dashboard'),
-        });
+      if (!newDashName) {
+        notify.error('You must pick a name for the new dashboard');
       } else {
-        data.dashboard_title = newDashboardTitle;
+        data.dashboard_title = newDashName;
         url = `/superset/copy_dash/${dashboardId}/`;
         this.saveDashboardRequest(data, url, saveType);
       }
     }
   }
+
   render() {
     return (
       <ModalTrigger
-        ref={(modal) => { this.modal = modal; }}
-        isMenuItem
+        ref={this.setModalRef}
+        isMenuItem={this.props.isMenuItem}
         triggerNode={this.props.triggerNode}
         modalTitle={t('Save Dashboard')}
         modalBody={
@@ -132,7 +158,7 @@ class SaveModal extends React.PureComponent {
             <div className="m-l-25 m-t-5">
               <Checkbox
                 checked={this.state.duplicateSlices}
-                onChange={this.toggleDuplicateSlices.bind(this)}
+                onChange={this.toggleDuplicateSlices}
               />
               <span className="m-l-5">also copy (duplicate) slices</span>
             </div>
@@ -140,10 +166,7 @@ class SaveModal extends React.PureComponent {
         }
         modalFooter={
           <div>
-            <Button
-              bsStyle="primary"
-              onClick={() => { this.saveDashboard(this.state.saveType, this.state.newDashName); }}
-            >
+            <Button bsStyle="primary" onClick={this.saveDashboard}>
               {t('Save')}
             </Button>
           </div>
@@ -152,6 +175,8 @@ class SaveModal extends React.PureComponent {
     );
   }
 }
+
 SaveModal.propTypes = propTypes;
+SaveModal.defaultProps = defaultProps;
 
 export default SaveModal;
diff --git a/superset/assets/src/dashboard/components/SliceAdder.jsx b/superset/assets/src/dashboard/components/SliceAdder.jsx
index 6477fc4..37ce21f 100644
--- a/superset/assets/src/dashboard/components/SliceAdder.jsx
+++ b/superset/assets/src/dashboard/components/SliceAdder.jsx
@@ -1,14 +1,15 @@
 import React from 'react';
 import PropTypes from 'prop-types';
-import cx from 'classnames';
 import { DropdownButton, MenuItem } from 'react-bootstrap';
 import { List } from 'react-virtualized';
 import SearchInput, { createFilter } from 'react-search-input';
 
-import DragDroppable from '../v2/components/dnd/DragDroppable';
-import { CHART_TYPE, NEW_COMPONENT_SOURCE_TYPE } from '../v2/util/componentTypes';
-import { NEW_CHART_ID, NEW_COMPONENTS_SOURCE_ID } from '../v2/util/constants';
-import { slicePropShape } from '../v2/util/propShapes';
+import AddSliceCard from './AddSliceCard';
+import AddSliceDragPreview from './dnd/AddSliceDragPreview';
+import DragDroppable from './dnd/DragDroppable';
+import { CHART_TYPE, NEW_COMPONENT_SOURCE_TYPE } from '../util/componentTypes';
+import { NEW_CHART_ID, NEW_COMPONENTS_SOURCE_ID } from '../util/constants';
+import { slicePropShape } from '../util/propShapes';
 
 const propTypes = {
   fetchAllSlices: PropTypes.func.isRequired,
@@ -24,6 +25,7 @@ const propTypes = {
 const defaultProps = {
   selectedSliceIds: new Set(),
   editMode: false,
+  errorMessage: '',
 };
 
 const KEYS_TO_FILTERS = ['slice_name', 'viz_type', 'datasource_name'];
@@ -35,12 +37,25 @@ const KEYS_TO_SORT = [
 ];
 
 class SliceAdder extends React.Component {
+  static sortByComparator(attr) {
+    const desc = attr === 'changed_on' ? -1 : 1;
+
+    return (a, b) => {
+      if (a[attr] < b[attr]) {
+        return -1 * desc;
+      } else if (a[attr] > b[attr]) {
+        return 1 * desc;
+      }
+      return 0;
+    };
+  }
+
   constructor(props) {
     super(props);
     this.state = {
       filteredSlices: [],
       searchTerm: '',
-      sortBy: KEYS_TO_SORT.findIndex(item => (item.key === 'changed_on')),
+      sortBy: KEYS_TO_SORT.findIndex(item => item.key === 'changed_on'),
     };
 
     this.rowRenderer = this.rowRenderer.bind(this);
@@ -58,7 +73,9 @@ class SliceAdder extends React.Component {
       this.setState({
         filteredSlices: Object.values(nextProps.slices)
           .filter(createFilter(this.state.searchTerm, KEYS_TO_FILTERS))
-          .sort(this.sortByComparator(KEYS_TO_SORT[this.state.sortBy].key)),
+          .sort(
+            SliceAdder.sortByComparator(KEYS_TO_SORT[this.state.sortBy].key),
+          ),
       });
     }
   }
@@ -72,20 +89,7 @@ class SliceAdder extends React.Component {
   getFilteredSortedSlices(searchTerm, sortBy) {
     return Object.values(this.props.slices)
       .filter(createFilter(searchTerm, KEYS_TO_FILTERS))
-      .sort(this.sortByComparator(KEYS_TO_SORT[sortBy].key));
-  }
-
-  sortByComparator(attr) {
-    const desc = (attr === 'changed_on') ? -1 : 1;
-
-    return (a, b) => {
-      if (a[attr] < b[attr]) {
-        return -1 * desc;
-      } else if (a[attr] > b[attr]) {
-        return 1 * desc;
-      }
-      return 0;
-    };
+      .sort(SliceAdder.sortByComparator(KEYS_TO_SORT[sortBy].key));
   }
 
   handleKeyPress(ev) {
@@ -99,20 +103,25 @@ class SliceAdder extends React.Component {
   searchUpdated(searchTerm) {
     this.setState({
       searchTerm,
-      filteredSlices: this.getFilteredSortedSlices(searchTerm, this.state.sortBy),
+      filteredSlices: this.getFilteredSortedSlices(
+        searchTerm,
+        this.state.sortBy,
+      ),
     });
   }
 
   handleSelect(sortBy) {
     this.setState({
       sortBy,
-      filteredSlices: this.getFilteredSortedSlices(this.state.searchTerm, sortBy),
+      filteredSlices: this.getFilteredSortedSlices(
+        this.state.searchTerm,
+        sortBy,
+      ),
     });
   }
 
   rowRenderer({ key, index, style }) {
     const cellData = this.state.filteredSlices[index];
-    const duration = cellData.modified ? cellData.modified.replace(/<[^>]*>/g, '') : '';
     const isSelected = this.props.selectedSliceIds.has(cellData.slice_id);
     const type = CHART_TYPE;
     const id = NEW_CHART_ID;
@@ -122,38 +131,32 @@ 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}
+        parentComponent={{
+          id: NEW_COMPONENTS_SOURCE_ID,
+          type: NEW_COMPONENT_SOURCE_TYPE,
+        }}
+        index={index}
         depth={0}
         disableDragDrop={isSelected}
         editMode={this.props.editMode}
+        // we must use a custom drag preview within the List because
+        // it does not seem to work within a fixed-position container
+        useEmptyDragPreview
       >
         {({ dragSourceRef }) => (
-          <div
-            ref={dragSourceRef}
-            className="chart-card-container"
-            key={key}
+          <AddSliceCard
+            innerRef={dragSourceRef}
             style={style}
-          >
-            <div className={cx('chart-card', { 'is-selected': isSelected })}>
-              <div className="card-title">{cellData.slice_name}</div>
-              <div className="card-body">
-                <div className="item">
-                  <span>Modified </span>
-                  <span>{duration}</span>
-                </div>
-                <div className="item">
-                  <span>Visualization </span>
-                  <span>{cellData.viz_type}</span>
-                </div>
-                <div className="item">
-                  <span>Data source </span>
-                  <span dangerouslySetInnerHTML={{ __html: cellData.datasource_link }} />
-                </div>
-              </div>
-            </div>
-          </div>
+            sliceName={cellData.slice_name}
+            lastModified={
+              cellData.modified ? cellData.modified.replace(/<[^>]*>/g, '') : ''
+            }
+            visType={cellData.viz_type}
+            datasourceLink={cellData.datasource_link}
+            isSelected={isSelected}
+          />
         )}
       </DragDroppable>
     );
@@ -169,7 +172,9 @@ class SliceAdder extends React.Component {
             id="slice-adder-sortby"
           >
             {KEYS_TO_SORT.map((item, index) => (
-              <MenuItem key={item.key} eventKey={index}>{item.label}</MenuItem>
+              <MenuItem key={item.key} eventKey={index}>
+                {item.label}
+              </MenuItem>
             ))}
           </DropdownButton>
 
@@ -179,18 +184,18 @@ class SliceAdder extends React.Component {
           />
         </div>
 
-        {this.props.isLoading &&
+        {this.props.isLoading && (
           <img
             src="/static/assets/images/loading.gif"
             className="loading"
             alt="loading"
           />
-        }
-        <div className={this.props.errorMessage ? '' : 'hidden'}>
-          {this.props.errorMessage}
-        </div>
-        <div className={!this.props.isLoading ? '' : 'hidden'}>
-          {this.state.filteredSlices.length > 0 &&
+        )}
+
+        {this.props.errorMessage && <div>{this.props.errorMessage}</div>}
+
+        {!this.props.isLoading &&
+          this.state.filteredSlices.length > 0 && (
             <List
               width={376}
               height={500}
@@ -201,8 +206,10 @@ class SliceAdder extends React.Component {
               sortBy={this.state.sortBy}
               selectedSliceIds={this.props.selectedSliceIds}
             />
-          }
-        </div>
+          )}
+
+        {/* Drag preview is just a single fixed-position element */}
+        <AddSliceDragPreview slices={this.state.filteredSlices} />
       </div>
     );
   }
diff --git a/superset/assets/src/dashboard/components/SliceAdderContainer.jsx b/superset/assets/src/dashboard/components/SliceAdderContainer.jsx
deleted file mode 100644
index b4f10d9..0000000
--- a/superset/assets/src/dashboard/components/SliceAdderContainer.jsx
+++ /dev/null
@@ -1,25 +0,0 @@
-import { bindActionCreators } from 'redux';
-import { connect } from 'react-redux';
-
-import { fetchAllSlices } from '../actions/sliceEntities';
-import SliceAdder from './SliceAdder';
-
-function mapStateToProps({ sliceEntities, dashboardInfo, dashboardState }) {
-  return {
-    userId: dashboardInfo.userId,
-    selectedSliceIds: dashboardState.sliceIds,
-    slices: sliceEntities.slices,
-    isLoading: sliceEntities.isLoading,
-    errorMessage: sliceEntities.errorMessage,
-    lastUpdated: sliceEntities.lastUpdated,
-    editMode: dashboardState.editMode,
-  };
-}
-
-function mapDispatchToProps(dispatch) {
-  return bindActionCreators({
-    fetchAllSlices
-  }, dispatch);
-}
-
-export default connect(mapStateToProps, mapDispatchToProps)(SliceAdder);
diff --git a/superset/assets/src/dashboard/components/SliceHeader.jsx b/superset/assets/src/dashboard/components/SliceHeader.jsx
index 264542a..bcdaedf 100644
--- a/superset/assets/src/dashboard/components/SliceHeader.jsx
+++ b/superset/assets/src/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: () => ({}),
@@ -29,6 +31,11 @@ const defaultProps = {
   exploreChart: () => ({}),
   exportCSV: () => ({}),
   editMode: false,
+  annotationQuery: {},
+  annotationError: {},
+  cachedDttm: null,
+  isCached: false,
+  isExpanded: false,
 };
 
 class SliceHeader extends React.PureComponent {
@@ -46,54 +53,62 @@ 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="col-md-12">
-          <div className="header">
-            <EditableTitle
-              title={slice.slice_name}
-              canEdit={!!this.props.updateSliceName && this.props.editMode}
-              onSaveTitle={this.onSaveTitle}
-              noPermitTooltip={'You don\'t have the rights to alter this dashboard.'}
-            />
-            {!!Object.values(this.props.annotationQuery || {}).length &&
-              <TooltipWrapper
-                label="annotations-loading"
-                placement="top"
-                tooltip={annoationsLoading}
-              >
-                <i className="fa fa-refresh warning" />
-              </TooltipWrapper>
-            }
-            {!!Object.values(this.props.annotationError || {}).length &&
-              <TooltipWrapper
-                label="annoation-errors"
-                placement="top"
-                tooltip={annoationsError}
-              >
-                <i className="fa fa-exclamation-circle danger" />
-              </TooltipWrapper>
+      <div className="chart-header" ref={innerRef}>
+        <div className="header">
+          <EditableTitle
+            title={slice.slice_name}
+            canEdit={!!this.props.updateSliceName && this.props.editMode}
+            onSaveTitle={this.onSaveTitle}
+            noPermitTooltip={
+              "You don't have the rights to alter this dashboard."
             }
-            {!this.props.editMode &&
-              <SliceHeaderControls
-                slice={slice}
-                isCached={isCached}
-                isExpanded={isExpanded}
-                cachedDttm={cachedDttm}
-                toggleExpandSlice={toggleExpandSlice}
-                forceRefresh={forceRefresh}
-                exploreChart={exploreChart}
-                exportCSV={exportCSV}
-              />
-            }
-          </div>
+            showTooltip={!!this.props.updateSliceName && this.props.editMode}
+          />
+          {!!Object.values(this.props.annotationQuery).length && (
+            <TooltipWrapper
+              label="annotations-loading"
+              placement="top"
+              tooltip={annoationsLoading}
+            >
+              <i className="fa fa-refresh warning" />
+            </TooltipWrapper>
+          )}
+          {!!Object.values(this.props.annotationError).length && (
+            <TooltipWrapper
+              label="annoation-errors"
+              placement="top"
+              tooltip={annoationsError}
+            >
+              <i className="fa fa-exclamation-circle danger" />
+            </TooltipWrapper>
+          )}
+          {!this.props.editMode && (
+            <SliceHeaderControls
+              slice={slice}
+              isCached={isCached}
+              isExpanded={isExpanded}
+              cachedDttm={cachedDttm}
+              toggleExpandSlice={toggleExpandSlice}
+              forceRefresh={forceRefresh}
+              exploreChart={exploreChart}
+              exportCSV={exportCSV}
+            />
+          )}
         </div>
       </div>
     );
diff --git a/superset/assets/src/dashboard/components/SliceHeaderControls.jsx b/superset/assets/src/dashboard/components/SliceHeaderControls.jsx
index e98f69e..ee1f261 100644
--- a/superset/assets/src/dashboard/components/SliceHeaderControls.jsx
+++ b/superset/assets/src/dashboard/components/SliceHeaderControls.jsx
@@ -23,14 +23,23 @@ const defaultProps = {
   toggleExpandSlice: () => ({}),
   exploreChart: () => ({}),
   exportCSV: () => ({}),
+  cachedDttm: null,
+  isCached: false,
+  isExpanded: false,
 };
 
 class SliceHeaderControls extends React.PureComponent {
   constructor(props) {
     super(props);
     this.exportCSV = this.props.exportCSV.bind(this, this.props.slice.slice_id);
-    this.exploreChart = this.props.exploreChart.bind(this, this.props.slice.slice_id);
-    this.toggleExpandSlice = this.props.toggleExpandSlice.bind(this, this.props.slice.slice_id);
+    this.exploreChart = this.props.exploreChart.bind(
+      this,
+      this.props.slice.slice_id,
+    );
+    this.toggleExpandSlice = this.props.toggleExpandSlice.bind(
+      this,
+      this.props.slice.slice_id,
+    );
     this.toggleControls = this.toggleControls.bind(this);
 
     this.state = {
@@ -48,15 +57,17 @@ class SliceHeaderControls extends React.PureComponent {
     const slice = this.props.slice;
     const isCached = this.props.isCached;
     const cachedWhen = moment.utc(this.props.cachedDttm).fromNow();
-    const refreshTooltip = isCached ?
-      t('Served from data cached %s . Click to force refresh.', cachedWhen) :
-      t('Force refresh data');
+    const refreshTooltip = isCached
+      ? t('Served from data cached %s . Click to force refresh.', cachedWhen)
+      : t('Force refresh data');
 
     return (
       <DropdownButton
         title=""
         id={`slice_${slice.slice_id}-controls`}
-        className={cx('slice-header-controls-trigger', 'fa fa-ellipsis-v', { 'is-cached': isCached })}
+        className={cx('slice-header-controls-trigger', 'fa fa-ellipsis-v', {
+          'is-cached': isCached,
+        })}
         pullRight
         noCaret
       >
@@ -66,17 +77,17 @@ class SliceHeaderControls extends React.PureComponent {
           onClick={this.props.forceRefresh}
         />
 
-        {slice.description &&
+        {slice.description && (
           <ActionMenuItem
             text={t('Toggle chart description')}
             tooltip={t('Toggle chart description')}
             onClick={this.toggleExpandSlice}
           />
-        }
+        )}
 
         <ActionMenuItem
           text={t('Edit chart')}
-          tooltip={t('Edit the chart\'s properties')}
+          tooltip={t("Edit the chart's properties")}
           href={slice.edit_url}
           target="_blank"
         />
diff --git a/superset/assets/src/dashboard/v2/components/Toast.jsx b/superset/assets/src/dashboard/components/Toast.jsx
similarity index 93%
rename from superset/assets/src/dashboard/v2/components/Toast.jsx
rename to superset/assets/src/dashboard/components/Toast.jsx
index 537388d..3c5a3ca 100644
--- a/superset/assets/src/dashboard/v2/components/Toast.jsx
+++ b/superset/assets/src/dashboard/components/Toast.jsx
@@ -4,7 +4,12 @@ import PropTypes from 'prop-types';
 import React from 'react';
 
 import { toastShape } from '../util/propShapes';
-import { INFO_TOAST, SUCCESS_TOAST, WARNING_TOAST, DANGER_TOAST } from '../util/constants';
+import {
+  INFO_TOAST,
+  SUCCESS_TOAST,
+  WARNING_TOAST,
+  DANGER_TOAST,
+} from '../util/constants';
 
 const propTypes = {
   toast: toastShape.isRequired,
@@ -60,7 +65,9 @@ class Toast extends React.Component {
 
   render() {
     const { visible } = this.state;
-    const { toast: { toastType, text } } = this.props;
+    const {
+      toast: { toastType, text },
+    } = this.props;
 
     return (
       <Alert
diff --git a/superset/assets/src/dashboard/v2/components/ToastPresenter.jsx b/superset/assets/src/dashboard/components/ToastPresenter.jsx
similarity index 81%
rename from superset/assets/src/dashboard/v2/components/ToastPresenter.jsx
rename to superset/assets/src/dashboard/components/ToastPresenter.jsx
index 95a0251..19d44b0 100644
--- a/superset/assets/src/dashboard/v2/components/ToastPresenter.jsx
+++ b/superset/assets/src/dashboard/components/ToastPresenter.jsx
@@ -19,16 +19,13 @@ class ToastPresenter extends React.Component {
     const { toasts, removeToast } = this.props;
 
     return (
-      toasts.length > 0 &&
+      toasts.length > 0 && (
         <div className="toast-presenter">
           {toasts.map(toast => (
-            <Toast
-              key={toast.id}
-              toast={toast}
-              onCloseToast={removeToast}
-            />
+            <Toast key={toast.id} toast={toast} onCloseToast={removeToast} />
           ))}
         </div>
+      )
     );
   }
 }
diff --git a/superset/assets/src/dashboard/components/dnd/AddSliceDragPreview.jsx b/superset/assets/src/dashboard/components/dnd/AddSliceDragPreview.jsx
new file mode 100644
index 0000000..94cab42
--- /dev/null
+++ b/superset/assets/src/dashboard/components/dnd/AddSliceDragPreview.jsx
@@ -0,0 +1,70 @@
+import React from 'react';
+import PropTypes from 'prop-types';
+import { DragLayer } from 'react-dnd';
+
+import AddSliceCard from '../AddSliceCard';
+import { slicePropShape } from '../../util/propShapes';
+import {
+  NEW_COMPONENT_SOURCE_TYPE,
+  CHART_TYPE,
+} from '../../util/componentTypes';
+
+const propTypes = {
+  dragItem: PropTypes.shape({
+    index: PropTypes.number.isRequired,
+  }),
+  slices: PropTypes.arrayOf(slicePropShape),
+  isDragging: PropTypes.bool.isRequired,
+  currentOffset: PropTypes.shape({
+    x: PropTypes.number.isRequired,
+    y: PropTypes.number.isRequired,
+  }),
+};
+
+const defaultProps = {
+  currentOffset: null,
+  dragItem: null,
+  slices: null,
+};
+
+function AddSliceDragPreview({ dragItem, slices, isDragging, currentOffset }) {
+  if (!isDragging || !currentOffset || !dragItem || !slices) return null;
+
+  const slice = slices[dragItem.index];
+
+  // make sure it's a new component and a chart
+  const shouldRender =
+    slice &&
+    dragItem.parentType === NEW_COMPONENT_SOURCE_TYPE &&
+    dragItem.type === CHART_TYPE;
+
+  return !shouldRender ? null : (
+    <AddSliceCard
+      style={{
+        position: 'fixed',
+        background: 'white',
+        pointerEvents: 'none',
+        top: 0,
+        left: 0,
+        zIndex: 100,
+        transform: `translate(${currentOffset.x}px, ${currentOffset.y}px)`,
+      }}
+      sliceName={slice.slice_name}
+      lastModified={
+        slice.modified ? slice.modified.replace(/<[^>]*>/g, '') : ''
+      }
+      visType={slice.viz_type}
+      datasourceLink={slice.datasource_link}
+    />
+  );
+}
+
+AddSliceDragPreview.propTypes = propTypes;
+AddSliceDragPreview.defaultProps = defaultProps;
+
+// This injects these props into the component
+export default DragLayer(monitor => ({
+  dragItem: monitor.getItem(),
+  currentOffset: monitor.getSourceClientOffset(),
+  isDragging: monitor.isDragging(),
+}))(AddSliceDragPreview);
diff --git a/superset/assets/src/dashboard/v2/components/dnd/DragDroppable.jsx b/superset/assets/src/dashboard/components/dnd/DragDroppable.jsx
similarity index 65%
rename from superset/assets/src/dashboard/v2/components/dnd/DragDroppable.jsx
rename to superset/assets/src/dashboard/components/dnd/DragDroppable.jsx
index 775e092..bfe4973 100644
--- a/superset/assets/src/dashboard/v2/components/dnd/DragDroppable.jsx
+++ b/superset/assets/src/dashboard/components/dnd/DragDroppable.jsx
@@ -1,3 +1,4 @@
+import { getEmptyImage } from 'react-dnd-html5-backend';
 import React from 'react';
 import PropTypes from 'prop-types';
 import { DragSource, DropTarget } from 'react-dnd';
@@ -5,7 +6,12 @@ import cx from 'classnames';
 
 import { componentShape } from '../../util/propShapes';
 import { dragConfig, dropConfig } from './dragDroppableConfig';
-import { DROP_TOP, DROP_RIGHT, DROP_BOTTOM, DROP_LEFT } from '../../util/getDropPosition';
+import {
+  DROP_TOP,
+  DROP_RIGHT,
+  DROP_BOTTOM,
+  DROP_LEFT,
+} from '../../util/getDropPosition';
 
 const propTypes = {
   children: PropTypes.func,
@@ -19,6 +25,7 @@ const propTypes = {
   style: PropTypes.object,
   onDrop: PropTypes.func,
   editMode: PropTypes.bool.isRequired,
+  useEmptyDragPreview: PropTypes.bool,
 
   // from react-dnd
   isDragging: PropTypes.bool.isRequired,
@@ -37,6 +44,7 @@ const defaultProps = {
   children() {},
   onDrop() {},
   orientation: 'row',
+  useEmptyDragPreview: false,
 };
 
 class DragDroppable extends React.Component {
@@ -58,7 +66,16 @@ class DragDroppable extends React.Component {
 
   setRef(ref) {
     this.ref = ref;
-    this.props.dragPreviewRef(ref);
+    // this is needed for a custom drag preview
+    if (this.props.useEmptyDragPreview) {
+      this.props.dragPreviewRef(getEmptyImage(), {
+        // IE fallback: specify that we'd rather screenshot the node
+        // when it already knows it's being dragged so we can hide it with CSS.
+        captureDraggingState: true,
+      });
+    } else {
+      this.props.dragPreviewRef(ref);
+    }
     this.props.droppableRef(ref);
   }
 
@@ -74,8 +91,6 @@ class DragDroppable extends React.Component {
       editMode,
     } = this.props;
 
-    if (!editMode) return children({});
-
     const { dropIndicator } = this.state;
 
     return (
@@ -90,18 +105,23 @@ class DragDroppable extends React.Component {
           className,
         )}
       >
-        {children({
-          dragSourceRef,
-          dropIndicatorProps: isDraggingOver && dropIndicator && {
-            className: cx(
-              'drop-indicator',
-              dropIndicator === DROP_TOP && 'drop-indicator--top',
-              dropIndicator === DROP_BOTTOM && 'drop-indicator--bottom',
-              dropIndicator === DROP_LEFT && 'drop-indicator--left',
-              dropIndicator === DROP_RIGHT && 'drop-indicator--right',
-            ),
-          },
-        })}
+        {children(
+          !editMode
+            ? {}
+            : {
+                dragSourceRef,
+                dropIndicatorProps: isDraggingOver &&
+                  dropIndicator && {
+                    className: cx(
+                      'drop-indicator',
+                      dropIndicator === DROP_TOP && 'drop-indicator--top',
+                      dropIndicator === DROP_BOTTOM && 'drop-indicator--bottom',
+                      dropIndicator === DROP_LEFT && 'drop-indicator--left',
+                      dropIndicator === DROP_RIGHT && 'drop-indicator--right',
+                    ),
+                  },
+              },
+        )}
       </div>
     );
   }
diff --git a/superset/assets/src/dashboard/v2/components/dnd/DragHandle.jsx b/superset/assets/src/dashboard/components/dnd/DragHandle.jsx
similarity index 82%
rename from superset/assets/src/dashboard/v2/components/dnd/DragHandle.jsx
rename to superset/assets/src/dashboard/components/dnd/DragHandle.jsx
index 36d1e6b..23d7d11 100644
--- a/superset/assets/src/dashboard/v2/components/dnd/DragHandle.jsx
+++ b/superset/assets/src/dashboard/components/dnd/DragHandle.jsx
@@ -26,9 +26,11 @@ export default class DragHandle extends React.PureComponent {
           position === 'top' && 'drag-handle--top',
         )}
       >
-        {Array(dotCount).fill(null).map((_, i) => (
-          <div key={`handle-dot-${i}`} className="drag-handle-dot" />
-        ))}
+        {Array(dotCount)
+          .fill(null)
+          .map((_, i) => (
+            <div key={`handle-dot-${i}`} className="drag-handle-dot" />
+          ))}
       </div>
     );
   }
diff --git a/superset/assets/src/dashboard/v2/components/dnd/dragDroppableConfig.js b/superset/assets/src/dashboard/components/dnd/dragDroppableConfig.js
similarity index 88%
rename from superset/assets/src/dashboard/v2/components/dnd/dragDroppableConfig.js
rename to superset/assets/src/dashboard/components/dnd/dragDroppableConfig.js
index 54ce67e..36c34a0 100644
--- a/superset/assets/src/dashboard/v2/components/dnd/dragDroppableConfig.js
+++ b/superset/assets/src/dashboard/components/dnd/dragDroppableConfig.js
@@ -38,15 +38,11 @@ export const dropConfig = [
   {
     hover(props, monitor, component) {
       if (
-        component
-        && component.decoratedComponentInstance
-        && component.decoratedComponentInstance.mounted
+        component &&
+        component.decoratedComponentInstance &&
+        component.decoratedComponentInstance.mounted
       ) {
-        handleHover(
-          props,
-          monitor,
-          component.decoratedComponentInstance,
-        );
+        handleHover(props, monitor, component.decoratedComponentInstance);
       }
     },
     // note:
diff --git a/superset/assets/src/dashboard/v2/components/dnd/handleDrop.js b/superset/assets/src/dashboard/components/dnd/handleDrop.js
similarity index 73%
rename from superset/assets/src/dashboard/v2/components/dnd/handleDrop.js
rename to superset/assets/src/dashboard/components/dnd/handleDrop.js
index 7cb630d..3739b18 100644
--- a/superset/assets/src/dashboard/v2/components/dnd/handleDrop.js
+++ b/superset/assets/src/dashboard/components/dnd/handleDrop.js
@@ -1,4 +1,9 @@
-import getDropPosition, { DROP_TOP, DROP_RIGHT, DROP_BOTTOM, DROP_LEFT } from '../../util/getDropPosition';
+import getDropPosition, {
+  DROP_TOP,
+  DROP_RIGHT,
+  DROP_BOTTOM,
+  DROP_LEFT,
+} from '../../util/getDropPosition';
 
 export default function handleDrop(props, monitor, Component) {
   // this may happen due to throttling
@@ -22,9 +27,12 @@ export default function handleDrop(props, monitor, Component) {
   const draggingItem = monitor.getItem();
 
   const dropAsChildOrSibling =
-    (orientation === 'row' && (dropPosition === DROP_TOP || dropPosition === DROP_BOTTOM)) ||
-    (orientation === 'column' && (dropPosition === DROP_LEFT || dropPosition === DROP_RIGHT))
-    ? 'sibling' : 'child';
+    (orientation === 'row' &&
+      (dropPosition === DROP_TOP || dropPosition === DROP_BOTTOM)) ||
+    (orientation === 'column' &&
+      (dropPosition === DROP_LEFT || dropPosition === DROP_RIGHT))
+      ? 'sibling'
+      : 'child';
 
   const dropResult = {
     source: {
@@ -49,8 +57,10 @@ export default function handleDrop(props, monitor, Component) {
   } else {
     // if the item is in the same list with a smaller index, you must account for the
     // "missing" index upon movement within the list
-    const sameParent = parentComponent && draggingItem.parentId === parentComponent.id;
-    const sameParentLowerIndex = sameParent && draggingItem.index < componentIndex;
+    const sameParent =
+      parentComponent && draggingItem.parentId === parentComponent.id;
+    const sameParentLowerIndex =
+      sameParent && draggingItem.index < componentIndex;
 
     let nextIndex = sameParentLowerIndex ? componentIndex - 1 : componentIndex;
     if (dropPosition === DROP_BOTTOM || dropPosition === DROP_RIGHT) {
diff --git a/superset/assets/src/dashboard/v2/components/dnd/handleHover.js b/superset/assets/src/dashboard/components/dnd/handleHover.js
similarity index 100%
rename from superset/assets/src/dashboard/v2/components/dnd/handleHover.js
rename to superset/assets/src/dashboard/components/dnd/handleHover.js
diff --git a/superset/assets/src/dashboard/components/gridComponents/Chart.jsx b/superset/assets/src/dashboard/components/gridComponents/Chart.jsx
new file mode 100644
index 0000000..54e1536
--- /dev/null
+++ b/superset/assets/src/dashboard/components/gridComponents/Chart.jsx
@@ -0,0 +1,233 @@
+import cx from 'classnames';
+import React from 'react';
+import PropTypes from 'prop-types';
+
+import { exportChart } from '../../../explore/exploreUtils';
+import SliceHeader from '../SliceHeader';
+import ChartContainer from '../../../chart/ChartContainer';
+import { chartPropType } from '../../../chart/chartReducer';
+import { slicePropShape } from '../../util/propShapes';
+import { VIZ_TYPES } from '../../../visualizations/main';
+
+const propTypes = {
+  id: PropTypes.number.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,
+};
+
+// we use state + shouldComponentUpdate() logic to prevent perf-wrecking
+// resizing across all slices on a dashboard on every update
+const RESIZE_TIMEOUT = 350;
+const SHOULD_UPDATE_ON_PROP_CHANGES = Object.keys(propTypes).filter(
+  prop => prop !== 'width' && prop !== 'height',
+);
+const OVERFLOWABLE_VIZ_TYPES = new Set([VIZ_TYPES.filter_box]);
+
+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) {
+    // this logic mostly pertains to chart resizing. we keep a copy of the dimensions in
+    // state so that we can buffer component size updates and only update on the final call
+    // which improves performance significantly
+    if (
+      nextState.width !== this.state.width ||
+      nextState.height !== this.state.height
+    ) {
+      return true;
+    }
+
+    for (let i = 0; i < SHOULD_UPDATE_ON_PROP_CHANGES.length; i += 1) {
+      const prop = SHOULD_UPDATE_ON_PROP_CHANGES[i];
+      if (nextProps[prop] !== this.props[prop]) {
+        return true;
+      }
+    }
+
+    if (
+      nextProps.width !== this.props.width ||
+      nextProps.height !== this.props.height
+    ) {
+      clearTimeout(this.resizeTimeout);
+      this.resizeTimeout = setTimeout(this.resize, RESIZE_TIMEOUT);
+    }
+
+    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;
+    const isOverflowable = OVERFLOWABLE_VIZ_TYPES.has(slice && slice.viz_type);
+
+    return (
+      <div
+        className={cx(
+          'dashboard-chart',
+          isOverflowable && 'dashboard-chart--overflowable',
+        )}
+      >
+        <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
+        */}
+        {isExpanded &&
+          slice.description_markeddown && (
+            <div
+              className="slice_description bs-callout bs-callout-default"
+              ref={this.setDescriptionRef}
+              // eslint-disable-next-line react/no-danger
+              dangerouslySetInnerHTML={{ __html: slice.description_markeddown }}
+            />
+          )}
+
+        <ChartContainer
+          containerId={`slice-container-${id}`}
+          chartId={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/src/dashboard/v2/components/gridComponents/ChartHolder.jsx b/superset/assets/src/dashboard/components/gridComponents/ChartHolder.jsx
similarity index 68%
rename from superset/assets/src/dashboard/v2/components/gridComponents/ChartHolder.jsx
rename to superset/assets/src/dashboard/components/gridComponents/ChartHolder.jsx
index 2aed4b2..a684230 100644
--- a/superset/assets/src/dashboard/v2/components/gridComponents/ChartHolder.jsx
+++ b/superset/assets/src/dashboard/components/gridComponents/ChartHolder.jsx
@@ -1,15 +1,21 @@
 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 CHART_MARGIN = 32;
 
 const propTypes = {
   id: PropTypes.string.isRequired,
@@ -19,7 +25,6 @@ const propTypes = {
   index: PropTypes.number.isRequired,
   depth: PropTypes.number.isRequired,
   editMode: PropTypes.bool.isRequired,
-  chart: PropTypes.object.isRequired,
 
   // grid related
   availableColumnCount: PropTypes.number.isRequired,
@@ -33,8 +38,7 @@ const propTypes = {
   handleComponentDrop: PropTypes.func.isRequired,
 };
 
-const defaultProps = {
-};
+const defaultProps = {};
 
 class ChartHolder extends React.Component {
   constructor(props) {
@@ -73,6 +77,12 @@ 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 +100,37 @@ 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
+              ref={dragSourceRef}
+              className="dashboard-component dashboard-component-chart-holder"
             >
-              <div className="dashboard-component dashboard-component-chart">
-                {this.props.chart}
-              </div>
-
-              {dropIndicatorProps && <div {...dropIndicatorProps} />}
-            </WithPopoverMenu>
+              <Chart
+                id={component.meta.chartId}
+                width={widthMultiple * columnWidth}
+                height={component.meta.height * GRID_BASE_UNIT - CHART_MARGIN}
+              />
+              {editMode && (
+                <HoverMenu position="top">
+                  <DragHandle position="top" />
+                  <DeleteComponentButton
+                    onDelete={this.handleDeleteComponent}
+                  />
+                </HoverMenu>
+              )}
+            </div>
+
+            {dropIndicatorProps && <div {...dropIndicatorProps} />}
           </ResizableContainer>
         )}
       </DragDroppable>
diff --git a/superset/assets/src/dashboard/v2/components/gridComponents/Column.jsx b/superset/assets/src/dashboard/components/gridComponents/Column.jsx
similarity index 82%
rename from superset/assets/src/dashboard/v2/components/gridComponents/Column.jsx
rename to superset/assets/src/dashboard/components/gridComponents/Column.jsx
index 490d7bd..a71d732 100644
--- a/superset/assets/src/dashboard/v2/components/gridComponents/Column.jsx
+++ b/superset/assets/src/dashboard/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,
@@ -41,8 +40,7 @@ const propTypes = {
   updateComponents: PropTypes.func.isRequired,
 };
 
-const defaultProps = {
-};
+const defaultProps = {};
 
 class Column extends React.PureComponent {
   constructor(props) {
@@ -50,7 +48,10 @@ class Column extends React.PureComponent {
     this.state = {
       isFocused: false,
     };
-    this.handleChangeBackground = this.handleUpdateMeta.bind(this, 'background');
+    this.handleChangeBackground = this.handleUpdateMeta.bind(
+      this,
+      'background',
+    );
     this.handleChangeFocus = this.handleChangeFocus.bind(this);
     this.handleDeleteComponent = this.handleDeleteComponent.bind(this);
   }
@@ -93,12 +94,13 @@ class Column extends React.PureComponent {
       onResizeStop,
       handleComponentDrop,
       editMode,
-      cells,
     } = this.props;
 
     const columnItems = columnComponent.children || [];
     const backgroundStyle = backgroundStyleOptions.find(
-      opt => opt.value === (columnComponent.meta.background || BACKGROUND_TRANSPARENT),
+      opt =>
+        opt.value ===
+        (columnComponent.meta.background || BACKGROUND_TRANSPARENT),
     );
 
     return (
@@ -119,7 +121,9 @@ class Column extends React.PureComponent {
             widthStep={columnWidth}
             widthMultiple={columnComponent.meta.width}
             minWidthMultiple={minColumnWidth}
-            maxWidthMultiple={availableColumnCount + (columnComponent.meta.width || 0)}
+            maxWidthMultiple={
+              availableColumnCount + (columnComponent.meta.width || 0)
+            }
             onResizeStart={onResizeStart}
             onResize={onResize}
             onResizeStop={onResizeStop}
@@ -145,31 +149,33 @@ class Column extends React.PureComponent {
                   backgroundStyle.className,
                 )}
               >
-                {editMode &&
+                {editMode && (
                   <HoverMenu innerRef={dragSourceRef} position="top">
                     <DragHandle position="top" />
-                    <DeleteComponentButton onDelete={this.handleDeleteComponent} />
+                    <DeleteComponentButton
+                      onDelete={this.handleDeleteComponent}
+                    />
                     <IconButton
                       onClick={this.handleChangeFocus}
                       className="fa fa-cog"
                     />
-                  </HoverMenu>}
+                  </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>
@@ -177,7 +183,6 @@ class Column extends React.PureComponent {
           </ResizableContainer>
         )}
       </DragDroppable>
-
     );
   }
 }
diff --git a/superset/assets/src/dashboard/v2/components/gridComponents/Divider.jsx b/superset/assets/src/dashboard/components/gridComponents/Divider.jsx
similarity index 96%
rename from superset/assets/src/dashboard/v2/components/gridComponents/Divider.jsx
rename to superset/assets/src/dashboard/components/gridComponents/Divider.jsx
index b3010e9..7c7936d 100644
--- a/superset/assets/src/dashboard/v2/components/gridComponents/Divider.jsx
+++ b/superset/assets/src/dashboard/components/gridComponents/Divider.jsx
@@ -51,10 +51,11 @@ class Divider extends React.PureComponent {
       >
         {({ dropIndicatorProps, dragSourceRef }) => (
           <div ref={dragSourceRef}>
-            {editMode &&
+            {editMode && (
               <HoverMenu position="left">
                 <DeleteComponentButton onDelete={this.handleDeleteComponent} />
-              </HoverMenu>}
+              </HoverMenu>
+            )}
 
             <div className="dashboard-component dashboard-component-divider" />
 
diff --git a/superset/assets/src/dashboard/v2/components/gridComponents/Header.jsx b/superset/assets/src/dashboard/components/gridComponents/Header.jsx
similarity index 93%
rename from superset/assets/src/dashboard/v2/components/gridComponents/Header.jsx
rename to superset/assets/src/dashboard/components/gridComponents/Header.jsx
index 97945a9..5114a77 100644
--- a/superset/assets/src/dashboard/v2/components/gridComponents/Header.jsx
+++ b/superset/assets/src/dashboard/components/gridComponents/Header.jsx
@@ -4,7 +4,7 @@ import cx from 'classnames';
 
 import DragDroppable from '../dnd/DragDroppable';
 import DragHandle from '../dnd/DragHandle';
-import EditableTitle from '../../../../components/EditableTitle';
+import EditableTitle from '../../../components/EditableTitle';
 import HoverMenu from '../menu/HoverMenu';
 import WithPopoverMenu from '../menu/WithPopoverMenu';
 import BackgroundStyleDropdown from '../menu/BackgroundStyleDropdown';
@@ -30,8 +30,7 @@ const propTypes = {
   updateComponents: PropTypes.func.isRequired,
 };
 
-const defaultProps = {
-};
+const defaultProps = {};
 
 class Header extends React.PureComponent {
   constructor(props) {
@@ -43,7 +42,10 @@ class Header extends React.PureComponent {
     this.handleChangeFocus = this.handleChangeFocus.bind(this);
     this.handleUpdateMeta = this.handleUpdateMeta.bind(this);
     this.handleChangeSize = this.handleUpdateMeta.bind(this, 'headerSize');
-    this.handleChangeBackground = this.handleUpdateMeta.bind(this, 'background');
+    this.handleChangeBackground = this.handleUpdateMeta.bind(
+      this,
+      'background',
+    );
     this.handleChangeText = this.handleUpdateMeta.bind(this, 'text');
   }
 
@@ -88,7 +90,8 @@ class Header extends React.PureComponent {
     );
 
     const rowStyle = backgroundStyleOptions.find(
-      opt => opt.value === (component.meta.background || BACKGROUND_TRANSPARENT),
+      opt =>
+        opt.value === (component.meta.background || BACKGROUND_TRANSPARENT),
     );
 
     return (
@@ -104,10 +107,11 @@ class Header extends React.PureComponent {
       >
         {({ dropIndicatorProps, dragSourceRef }) => (
           <div ref={dragSourceRef}>
-            {editMode &&
+            {editMode && (
               <HoverMenu position="left">
                 <DragHandle position="left" />
-              </HoverMenu>}
+              </HoverMenu>
+            )}
 
             <WithPopoverMenu
               onChangeFocus={this.handleChangeFocus}
diff --git a/superset/assets/src/dashboard/v2/components/gridComponents/Row.jsx b/superset/assets/src/dashboard/components/gridComponents/Row.jsx
similarity index 82%
rename from superset/assets/src/dashboard/v2/components/gridComponents/Row.jsx
rename to superset/assets/src/dashboard/components/gridComponents/Row.jsx
index 8faaee1..91f200d 100644
--- a/superset/assets/src/dashboard/v2/components/gridComponents/Row.jsx
+++ b/superset/assets/src/dashboard/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,
@@ -51,7 +50,10 @@ class Row extends React.PureComponent {
     };
     this.handleDeleteComponent = this.handleDeleteComponent.bind(this);
     this.handleUpdateMeta = this.handleUpdateMeta.bind(this);
-    this.handleChangeBackground = this.handleUpdateMeta.bind(this, 'background');
+    this.handleChangeBackground = this.handleUpdateMeta.bind(
+      this,
+      'background',
+    );
     this.handleChangeFocus = this.handleChangeFocus.bind(this);
   }
 
@@ -93,13 +95,13 @@ class Row extends React.PureComponent {
       onResizeStop,
       handleComponentDrop,
       editMode,
-      cells,
     } = this.props;
 
     const rowItems = rowComponent.children || [];
 
     const backgroundStyle = backgroundStyleOptions.find(
-      opt => opt.value === (rowComponent.meta.background || BACKGROUND_TRANSPARENT),
+      opt =>
+        opt.value === (rowComponent.meta.background || BACKGROUND_TRANSPARENT),
     );
 
     return (
@@ -133,31 +135,35 @@ class Row extends React.PureComponent {
                 backgroundStyle.className,
               )}
             >
-              {editMode &&
+              {editMode && (
                 <HoverMenu innerRef={dragSourceRef} position="left">
                   <DragHandle position="left" />
-                  <DeleteComponentButton onDelete={this.handleDeleteComponent} />
+                  <DeleteComponentButton
+                    onDelete={this.handleDeleteComponent}
+                  />
                   <IconButton
                     onClick={this.handleChangeFocus}
                     className="fa fa-cog"
                   />
-                </HoverMenu>}
+                </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/src/dashboard/v2/components/gridComponents/Tab.jsx b/superset/assets/src/dashboard/components/gridComponents/Tab.jsx
similarity index 90%
rename from superset/assets/src/dashboard/v2/components/gridComponents/Tab.jsx
rename to superset/assets/src/dashboard/components/gridComponents/Tab.jsx
index 218c4e7..d73bc0c 100644
--- a/superset/assets/src/dashboard/v2/components/gridComponents/Tab.jsx
+++ b/superset/assets/src/dashboard/components/gridComponents/Tab.jsx
@@ -3,7 +3,7 @@ import PropTypes from 'prop-types';
 
 import DashboardComponent from '../../containers/DashboardComponent';
 import DragDroppable from '../dnd/DragDroppable';
-import EditableTitle from '../../../../components/EditableTitle';
+import EditableTitle from '../../../components/EditableTitle';
 import DeleteComponentButton from '../DeleteComponentButton';
 import WithPopoverMenu from '../menu/WithPopoverMenu';
 import { componentShape } from '../../util/propShapes';
@@ -123,13 +123,7 @@ export default class Tab extends React.PureComponent {
 
   renderTab() {
     const { isFocused } = this.state;
-    const {
-      component,
-      parentComponent,
-      index,
-      depth,
-      editMode,
-    } = this.props;
+    const { component, parentComponent, index, depth, editMode } = this.props;
 
     return (
       <DragDroppable
@@ -149,9 +143,15 @@ export default class Tab extends React.PureComponent {
           <div className="dragdroppable-tab" ref={dragSourceRef}>
             <WithPopoverMenu
               onChangeFocus={this.handleChangeFocus}
-              menuItems={parentComponent.children.length <= 1 ? [] : [
-                <DeleteComponentButton onDelete={this.handleDeleteComponent} />,
-              ]}
+              menuItems={
+                parentComponent.children.length <= 1
+                  ? []
+                  : [
+                      <DeleteComponentButton
+                        onDelete={this.handleDeleteComponent}
+                      />,
+                    ]
+              }
               editMode={editMode}
             >
               <EditableTitle
@@ -171,7 +171,9 @@ export default class Tab extends React.PureComponent {
 
   render() {
     const { renderType } = this.props;
-    return renderType === RENDER_TAB ? this.renderTab() : this.renderTabContent();
+    return renderType === RENDER_TAB
+      ? this.renderTab()
+      : this.renderTabContent();
   }
 }
 
diff --git a/superset/assets/src/dashboard/v2/components/gridComponents/Tabs.jsx b/superset/assets/src/dashboard/components/gridComponents/Tabs.jsx
similarity index 81%
rename from superset/assets/src/dashboard/v2/components/gridComponents/Tabs.jsx
rename to superset/assets/src/dashboard/components/gridComponents/Tabs.jsx
index 1f5f0c6..585041f 100644
--- a/superset/assets/src/dashboard/v2/components/gridComponents/Tabs.jsx
+++ b/superset/assets/src/dashboard/components/gridComponents/Tabs.jsx
@@ -106,9 +106,10 @@ class Tabs extends React.PureComponent {
     // Ensure dropped tab is visible
     const { destination } = dropResult;
     if (destination) {
-      const dropTabIndex = destination.id === component.id
-        ? destination.index // dropped ON tabs
-        : component.children.indexOf(destination.id); // dropped IN tab
+      const dropTabIndex =
+        destination.id === component.id
+          ? destination.index // dropped ON tabs
+          : component.children.indexOf(destination.id); // dropped IN tab
 
       if (dropTabIndex > -1) {
         setTimeout(() => {
@@ -147,13 +148,17 @@ class Tabs extends React.PureComponent {
         onDrop={handleComponentDrop}
         editMode={editMode}
       >
-        {({ dropIndicatorProps: tabsDropIndicatorProps, dragSourceRef: tabsDragSourceRef }) => (
+        {({
+          dropIndicatorProps: tabsDropIndicatorProps,
+          dragSourceRef: tabsDragSourceRef,
+        }) => (
           <div className="dashboard-component dashboard-component-tabs">
-            {editMode &&
+            {editMode && (
               <HoverMenu innerRef={tabsDragSourceRef} position="left">
                 <DragHandle position="left" />
                 <DeleteComponentButton onDelete={this.handleDeleteComponent} />
-              </HoverMenu>}
+              </HoverMenu>
+            )}
 
             <BootstrapTabs
               id={tabsComponent.id}
@@ -187,37 +192,39 @@ class Tabs extends React.PureComponent {
                     render potentially-expensive charts (this also enables lazy loading
                     their content)
                   */}
-                  {tabIndex === selectedTabIndex && renderTabContent &&
-                    <DashboardComponent
-                      id={tabId}
-                      parentId={tabsComponent.id}
-                      depth={depth} // see isValidChild.js for why tabs don't increment child depth
-                      index={tabIndex}
-                      renderType={RENDER_TAB_CONTENT}
-                      availableColumnCount={availableColumnCount}
-                      columnWidth={columnWidth}
-                      onResizeStart={onResizeStart}
-                      onResize={onResize}
-                      onResizeStop={onResizeStop}
-                      onDropOnTab={this.handleDropOnTab}
-                    />}
+                  {tabIndex === selectedTabIndex &&
+                    renderTabContent && (
+                      <DashboardComponent
+                        id={tabId}
+                        parentId={tabsComponent.id}
+                        depth={depth} // see isValidChild.js for why tabs don't increment child depth
+                        index={tabIndex}
+                        renderType={RENDER_TAB_CONTENT}
+                        availableColumnCount={availableColumnCount}
+                        columnWidth={columnWidth}
+                        onResizeStart={onResizeStart}
+                        onResize={onResize}
+                        onResizeStop={onResizeStop}
+                        onDropOnTab={this.handleDropOnTab}
+                      />
+                    )}
                 </BootstrapTab>
               ))}
 
               {editMode &&
-                tabIds.length < MAX_TAB_COUNT &&
+                tabIds.length < MAX_TAB_COUNT && (
                   <BootstrapTab
                     eventKey={NEW_TAB_INDEX}
                     title={<div className="fa fa-plus" />}
-                  />}
-
+                  />
+                )}
             </BootstrapTabs>
 
             {/* don't indicate that a drop on root is allowed when tabs already exist */}
-            {tabsDropIndicatorProps
-              && parentComponent.id !== DASHBOARD_ROOT_ID
-              && <div {...tabsDropIndicatorProps} />}
-
+            {tabsDropIndicatorProps &&
+              parentComponent.id !== DASHBOARD_ROOT_ID && (
+                <div {...tabsDropIndicatorProps} />
+              )}
           </div>
         )}
       </DragDroppable>
diff --git a/superset/assets/src/dashboard/v2/components/gridComponents/index.js b/superset/assets/src/dashboard/components/gridComponents/index.js
similarity index 94%
rename from superset/assets/src/dashboard/v2/components/gridComponents/index.js
rename to superset/assets/src/dashboard/components/gridComponents/index.js
index ef6d13f..016ab03 100644
--- a/superset/assets/src/dashboard/v2/components/gridComponents/index.js
+++ b/superset/assets/src/dashboard/components/gridComponents/index.js
@@ -3,7 +3,6 @@ import {
   COLUMN_TYPE,
   DIVIDER_TYPE,
   HEADER_TYPE,
-  INVISIBLE_ROW_TYPE,
   ROW_TYPE,
   TAB_TYPE,
   TABS_TYPE,
@@ -30,7 +29,6 @@ export default {
   [COLUMN_TYPE]: Column,
   [DIVIDER_TYPE]: Divider,
   [HEADER_TYPE]: Header,
-  [INVISIBLE_ROW_TYPE]: Row,
   [ROW_TYPE]: Row,
   [TAB_TYPE]: Tab,
   [TABS_TYPE]: Tabs,
diff --git a/superset/assets/src/dashboard/v2/components/gridComponents/new/DraggableNewComponent.jsx b/superset/assets/src/dashboard/components/gridComponents/new/DraggableNewComponent.jsx
similarity index 90%
rename from superset/assets/src/dashboard/v2/components/gridComponents/new/DraggableNewComponent.jsx
rename to superset/assets/src/dashboard/components/gridComponents/new/DraggableNewComponent.jsx
index eebd6e0..d579dc1 100644
--- a/superset/assets/src/dashboard/v2/components/gridComponents/new/DraggableNewComponent.jsx
+++ b/superset/assets/src/dashboard/components/gridComponents/new/DraggableNewComponent.jsx
@@ -23,7 +23,10 @@ export default class DraggableNewComponent extends React.PureComponent {
     return (
       <DragDroppable
         component={{ type, id }}
-        parentComponent={{ id: NEW_COMPONENTS_SOURCE_ID, type: NEW_COMPONENT_SOURCE_TYPE }}
+        parentComponent={{
+          id: NEW_COMPONENTS_SOURCE_ID,
+          type: NEW_COMPONENT_SOURCE_TYPE,
+        }}
         index={0}
         depth={0}
         editMode
diff --git a/superset/assets/src/dashboard/components/gridComponents/new/NewColumn.jsx b/superset/assets/src/dashboard/components/gridComponents/new/NewColumn.jsx
new file mode 100644
index 0000000..f624e58
--- /dev/null
+++ b/superset/assets/src/dashboard/components/gridComponents/new/NewColumn.jsx
@@ -0,0 +1,16 @@
+import React from 'react';
+
+import { COLUMN_TYPE } from '../../../util/componentTypes';
+import { NEW_COLUMN_ID } from '../../../util/constants';
+import DraggableNewComponent from './DraggableNewComponent';
+
+export default function DraggableNewColumn() {
+  return (
+    <DraggableNewComponent
+      id={NEW_COLUMN_ID}
+      type={COLUMN_TYPE}
+      label="Column"
+      className="fa fa-long-arrow-down"
+    />
+  );
+}
diff --git a/superset/assets/src/dashboard/components/gridComponents/new/NewDivider.jsx b/superset/assets/src/dashboard/components/gridComponents/new/NewDivider.jsx
new file mode 100644
index 0000000..de07a24
--- /dev/null
+++ b/superset/assets/src/dashboard/components/gridComponents/new/NewDivider.jsx
@@ -0,0 +1,16 @@
+import React from 'react';
+
+import { DIVIDER_TYPE } from '../../../util/componentTypes';
+import { NEW_DIVIDER_ID } from '../../../util/constants';
+import DraggableNewComponent from './DraggableNewComponent';
+
+export default function DraggableNewDivider() {
+  return (
+    <DraggableNewComponent
+      id={NEW_DIVIDER_ID}
+      type={DIVIDER_TYPE}
+      label="Divider"
+      className="divider-placeholder"
+    />
+  );
+}
diff --git a/superset/assets/src/dashboard/components/gridComponents/new/NewHeader.jsx b/superset/assets/src/dashboard/components/gridComponents/new/NewHeader.jsx
new file mode 100644
index 0000000..50bd600
--- /dev/null
+++ b/superset/assets/src/dashboard/components/gridComponents/new/NewHeader.jsx
@@ -0,0 +1,16 @@
+import React from 'react';
+
+import { HEADER_TYPE } from '../../../util/componentTypes';
+import { NEW_HEADER_ID } from '../../../util/constants';
+import DraggableNewComponent from './DraggableNewComponent';
+
+export default function DraggableNewHeader() {
+  return (
+    <DraggableNewComponent
+      id={NEW_HEADER_ID}
+      type={HEADER_TYPE}
+      label="Header"
+      className="fa fa-header"
+    />
+  );
+}
diff --git a/superset/assets/src/dashboard/components/gridComponents/new/NewRow.jsx b/superset/assets/src/dashboard/components/gridComponents/new/NewRow.jsx
new file mode 100644
index 0000000..81bdc93
--- /dev/null
+++ b/superset/assets/src/dashboard/components/gridComponents/new/NewRow.jsx
@@ -0,0 +1,16 @@
+import React from 'react';
+
+import { ROW_TYPE } from '../../../util/componentTypes';
+import { NEW_ROW_ID } from '../../../util/constants';
+import DraggableNewComponent from './DraggableNewComponent';
+
+export default function DraggableNewRow() {
+  return (
+    <DraggableNewComponent
+      id={NEW_ROW_ID}
+      type={ROW_TYPE}
+      label="Row"
+      className="fa fa-long-arrow-right"
+    />
+  );
+}
diff --git a/superset/assets/src/dashboard/components/gridComponents/new/NewTabs.jsx b/superset/assets/src/dashboard/components/gridComponents/new/NewTabs.jsx
new file mode 100644
index 0000000..fd9366b
--- /dev/null
+++ b/superset/assets/src/dashboard/components/gridComponents/new/NewTabs.jsx
@@ -0,0 +1,16 @@
+import React from 'react';
+
+import { TABS_TYPE } from '../../../util/componentTypes';
+import { NEW_TABS_ID } from '../../../util/constants';
+import DraggableNewComponent from './DraggableNewComponent';
+
+export default function DraggableNewTabs() {
+  return (
+    <DraggableNewComponent
+      id={NEW_TABS_ID}
+      type={TABS_TYPE}
+      label="Tabs"
+      className="fa fa-window-restore"
+    />
+  );
+}
diff --git a/superset/assets/src/dashboard/v2/components/menu/BackgroundStyleDropdown.jsx b/superset/assets/src/dashboard/components/menu/BackgroundStyleDropdown.jsx
similarity index 100%
rename from superset/assets/src/dashboard/v2/components/menu/BackgroundStyleDropdown.jsx
rename to superset/assets/src/dashboard/components/menu/BackgroundStyleDropdown.jsx
diff --git a/superset/assets/src/dashboard/v2/components/menu/HoverMenu.jsx b/superset/assets/src/dashboard/components/menu/HoverMenu.jsx
similarity index 100%
rename from superset/assets/src/dashboard/v2/components/menu/HoverMenu.jsx
rename to superset/assets/src/dashboard/components/menu/HoverMenu.jsx
diff --git a/superset/assets/src/dashboard/v2/components/menu/PopoverDropdown.jsx b/superset/assets/src/dashboard/components/menu/PopoverDropdown.jsx
similarity index 94%
rename from superset/assets/src/dashboard/v2/components/menu/PopoverDropdown.jsx
rename to superset/assets/src/dashboard/components/menu/PopoverDropdown.jsx
index 6a56eab..4971793 100644
--- a/superset/assets/src/dashboard/v2/components/menu/PopoverDropdown.jsx
+++ b/superset/assets/src/dashboard/components/menu/PopoverDropdown.jsx
@@ -19,7 +19,9 @@ const propTypes = {
 
 const defaultProps = {
   renderButton: option => option.label,
-  renderOption: option => <div className={option.className}>{option.label}</div>,
+  renderOption: option => (
+    <div className={option.className}>{option.label}</div>
+  ),
 };
 
 class PopoverDropdown extends React.PureComponent {
diff --git a/superset/assets/src/dashboard/v2/components/menu/WithPopoverMenu.jsx b/superset/assets/src/dashboard/components/menu/WithPopoverMenu.jsx
similarity index 91%
rename from superset/assets/src/dashboard/v2/components/menu/WithPopoverMenu.jsx
rename to superset/assets/src/dashboard/components/menu/WithPopoverMenu.jsx
index f213442..8a87fca 100644
--- a/superset/assets/src/dashboard/v2/components/menu/WithPopoverMenu.jsx
+++ b/superset/assets/src/dashboard/components/menu/WithPopoverMenu.jsx
@@ -54,12 +54,15 @@ 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
@@ -97,12 +100,15 @@ class WithPopoverMenu extends React.PureComponent {
         {children}
         {editMode &&
           isFocused &&
-          menuItems.length > 0 &&
-            <div className="popover-menu" >
+          menuItems.length > 0 && (
+            <div className="popover-menu">
               {menuItems.map((node, i) => (
-                <div className="menu-item" key={`menu-item-${i}`}>{node}</div>
+                <div className="menu-item" key={`menu-item-${i}`}>
+                  {node}
+                </div>
               ))}
-            </div>}
+            </div>
+          )}
       </div>
     );
   }
diff --git a/superset/assets/src/dashboard/v2/components/resizable/ResizableContainer.jsx b/superset/assets/src/dashboard/components/resizable/ResizableContainer.jsx
similarity index 78%
rename from superset/assets/src/dashboard/v2/components/resizable/ResizableContainer.jsx
rename to superset/assets/src/dashboard/components/resizable/ResizableContainer.jsx
index a532ff0..7e09e73 100644
--- a/superset/assets/src/dashboard/v2/components/resizable/ResizableContainer.jsx
+++ b/superset/assets/src/dashboard/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);
@@ -139,29 +142,26 @@ class ResizableContainer extends React.PureComponent {
 
     const size = {
       width: adjustableWidth
-        ? ((widthStep + gutterWidth) * widthMultiple) - gutterWidth
-        : (staticWidthMultiple && staticWidthMultiple * widthStep)
-          || staticWidth
-          || undefined,
+        ? (widthStep + gutterWidth) * widthMultiple - gutterWidth
+        : (staticWidthMultiple && staticWidthMultiple * widthStep) ||
+          staticWidth ||
+          undefined,
       height: adjustableHeight
         ? heightStep * heightMultiple
-        : (staticHeightMultiple && staticHeightMultiple * heightStep)
-          || staticHeight
-          || undefined,
+        : (staticHeightMultiple && staticHeightMultiple * heightStep) ||
+          staticHeight ||
+          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;
 
@@ -169,18 +169,27 @@ class ResizableContainer extends React.PureComponent {
       <Resizable
         enable={enableConfig}
         grid={SNAP_TO_GRID}
-        minWidth={adjustableWidth
-          ? (minWidthMultiple * (widthStep + gutterWidth)) - gutterWidth
-          : undefined}
-        minHeight={adjustableHeight
-          ? (minHeightMultiple * heightStep)
-          : undefined}
-        maxWidth={adjustableWidth
-          ? Math.max(size.width, (maxWidthMultiple * (widthStep + gutterWidth)) - gutterWidth)
-          : undefined}
-        maxHeight={adjustableHeight
-          ? Math.max(size.height, maxHeightMultiple * heightStep)
-          : undefined}
+        minWidth={
+          adjustableWidth
+            ? minWidthMultiple * (widthStep + gutterWidth) - gutterWidth
+            : undefined
+        }
+        minHeight={
+          adjustableHeight ? minHeightMultiple * heightStep : undefined
+        }
+        maxWidth={
+          adjustableWidth
+            ? Math.max(
+                size.width,
+                maxWidthMultiple * (widthStep + gutterWidth) - gutterWidth,
+              )
+            : undefined
+        }
+        maxHeight={
+          adjustableHeight
+            ? Math.max(size.height, maxHeightMultiple * heightStep)
+            : undefined
+        }
         size={size}
         onResizeStart={this.handleResizeStart}
         onResize={this.handleResize}
@@ -190,6 +199,7 @@ class ResizableContainer extends React.PureComponent {
           'resizable-container',
           isResizing && 'resizable-container--resizing',
         )}
+        handleClasses={HANDLE_CLASSES}
       >
         {children}
       </Resizable>
diff --git a/superset/assets/src/dashboard/v2/components/resizable/ResizableHandle.jsx b/superset/assets/src/dashboard/components/resizable/ResizableHandle.jsx
similarity index 54%
rename from superset/assets/src/dashboard/v2/components/resizable/ResizableHandle.jsx
rename to superset/assets/src/dashboard/components/resizable/ResizableHandle.jsx
index 9536f6b..b696b26 100644
--- a/superset/assets/src/dashboard/v2/components/resizable/ResizableHandle.jsx
+++ b/superset/assets/src/dashboard/components/resizable/ResizableHandle.jsx
@@ -1,21 +1,15 @@
 import React from 'react';
 
 export function BottomRightResizeHandle() {
-  return (
-    <div className="resize-handle resize-handle--bottom-right" />
-  );
+  return <div className="resize-handle resize-handle--bottom-right" />;
 }
 
 export function RightResizeHandle() {
-  return (
-    <div className="resize-handle resize-handle--right" />
-  );
+  return <div className="resize-handle resize-handle--right" />;
 }
 
 export function BottomResizeHandle() {
-  return (
-    <div className="resize-handle resize-handle--bottom" />
-  );
+  return <div className="resize-handle resize-handle--bottom" />;
 }
 
 export default {
diff --git a/superset/assets/src/dashboard/containers/Chart.jsx b/superset/assets/src/dashboard/containers/Chart.jsx
new file mode 100644
index 0000000..470176b
--- /dev/null
+++ b/superset/assets/src/dashboard/containers/Chart.jsx
@@ -0,0 +1,59 @@
+import { bindActionCreators } from 'redux';
+import { connect } from 'react-redux';
+
+import {
+  addFilter,
+  removeFilter,
+  toggleExpandSlice,
+} from '../actions/dashboardState';
+import { refreshChart } from '../../chart/chartAction';
+import getFormDataWithExtraFilters from '../util/charts/getFormDataWithExtraFilters';
+import { saveSliceName } from '../actions/sliceEntities';
+import Chart from '../components/gridComponents/Chart';
+
+function mapStateToProps(
+  {
+    charts: chartQueries,
+    dashboardInfo,
+    dashboardState,
+    datasources,
+    sliceEntities,
+  },
+  ownProps,
+) {
+  const { id } = ownProps;
+  const chart = chartQueries[id];
+  const { filters } = dashboardState;
+
+  return {
+    chart,
+    datasource: datasources[chart.form_data.datasource],
+    slice: sliceEntities.slices[id],
+    timeout: dashboardInfo.common.conf.SUPERSET_WEBSERVER_TIMEOUT,
+    filters,
+    // note: this method caches filters if possible to prevent render cascades
+    formData: getFormDataWithExtraFilters({
+      chart,
+      dashboardMetadata: dashboardInfo.metadata,
+      filters,
+      sliceId: id,
+    }),
+    editMode: dashboardState.editMode,
+    isExpanded: !!dashboardState.expandedSlices[id],
+  };
+}
+
+function mapDispatchToProps(dispatch) {
+  return bindActionCreators(
+    {
+      saveSliceName,
+      toggleExpandSlice,
+      addFilter,
+      refreshChart,
+      removeFilter,
+    },
+    dispatch,
+  );
+}
+
+export default connect(mapStateToProps, mapDispatchToProps)(Chart);
diff --git a/superset/assets/src/dashboard/containers/Dashboard.jsx b/superset/assets/src/dashboard/containers/Dashboard.jsx
new file mode 100644
index 0000000..9af0e81
--- /dev/null
+++ b/superset/assets/src/dashboard/containers/Dashboard.jsx
@@ -0,0 +1,49 @@
+import { bindActionCreators } from 'redux';
+import { connect } from 'react-redux';
+
+import {
+  addSliceToDashboard,
+  removeSliceFromDashboard,
+  onChange,
+} from '../actions/dashboardState';
+import { runQuery } from '../../chart/chartAction';
+import Dashboard from '../components/Dashboard';
+
+function mapStateToProps({
+  datasources,
+  sliceEntities,
+  charts,
+  dashboardInfo,
+  dashboardState,
+  dashboardLayout,
+  impressionId,
+}) {
+  return {
+    initMessages: dashboardInfo.common.flash_messages,
+    timeout: dashboardInfo.common.conf.SUPERSET_WEBSERVER_TIMEOUT,
+    userId: dashboardInfo.userId,
+    dashboardInfo,
+    dashboardState,
+    charts,
+    datasources,
+    slices: sliceEntities.slices,
+    layout: dashboardLayout.present,
+    impressionId,
+  };
+}
+
+function mapDispatchToProps(dispatch) {
+  return {
+    actions: bindActionCreators(
+      {
+        addSliceToDashboard,
+        onChange,
+        removeSliceFromDashboard,
+        runQuery,
+      },
+      dispatch,
+    ),
+  };
+}
+
+export default connect(mapStateToProps, mapDispatchToProps)(Dashboard);
diff --git a/superset/assets/src/dashboard/v2/containers/DashboardBuilder.jsx b/superset/assets/src/dashboard/containers/DashboardBuilder.jsx
similarity index 66%
rename from superset/assets/src/dashboard/v2/containers/DashboardBuilder.jsx
rename to superset/assets/src/dashboard/containers/DashboardBuilder.jsx
index 62fc94a..6bece3d 100644
--- a/superset/assets/src/dashboard/v2/containers/DashboardBuilder.jsx
+++ b/superset/assets/src/dashboard/containers/DashboardBuilder.jsx
@@ -7,20 +7,22 @@ import {
   handleComponentDrop,
 } from '../actions/dashboardLayout';
 
-function mapStateToProps({ dashboardLayout: undoableLayout, dashboardState: dashboard }, ownProps) {
+function mapStateToProps({ dashboardLayout: undoableLayout, dashboardState }) {
   return {
     dashboardLayout: undoableLayout.present,
-    cells: ownProps.cells,
-    editMode: dashboard.editMode,
-    showBuilderPane: dashboard.showBuilderPane,
+    editMode: dashboardState.editMode,
+    showBuilderPane: dashboardState.showBuilderPane,
   };
 }
 
 function mapDispatchToProps(dispatch) {
-  return bindActionCreators({
-    deleteTopLevelTabs,
-    handleComponentDrop,
-  }, dispatch);
+  return bindActionCreators(
+    {
+      deleteTopLevelTabs,
+      handleComponentDrop,
+    },
+    dispatch,
+  );
 }
 
 export default connect(mapStateToProps, mapDispatchToProps)(DashboardBuilder);
diff --git a/superset/assets/src/dashboard/v2/containers/DashboardComponent.jsx b/superset/assets/src/dashboard/containers/DashboardComponent.jsx
similarity index 73%
rename from superset/assets/src/dashboard/v2/containers/DashboardComponent.jsx
rename to superset/assets/src/dashboard/containers/DashboardComponent.jsx
index 01f7805..650313e 100644
--- a/superset/assets/src/dashboard/v2/containers/DashboardComponent.jsx
+++ b/superset/assets/src/dashboard/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,24 +25,36 @@ const propTypes = {
   handleComponentDrop: PropTypes.func.isRequired,
 };
 
-function mapStateToProps({ dashboardLayout: undoableLayout, dashboardState: dashboard }, ownProps) {
+function mapStateToProps(
+  {
+    dashboardLayout: undoableLayout,
+    dashboardState,
+    sliceEntities,
+    charts,
+    datasources,
+  },
+  ownProps,
+) {
   const dashboardLayout = undoableLayout.present;
-  const { id, parentId, cells } = ownProps;
+  const { id, parentId } = ownProps;
   const component = dashboardLayout[id];
   const props = {
     component,
     parentComponent: dashboardLayout[parentId],
-    editMode: dashboard.editMode,
+    editMode: dashboardState.editMode,
   };
 
   // rows and columns need more data about their child dimensions
   // doing this allows us to not pass the entire component lookup to all Components
   if (props.component.type === ROW_TYPE) {
-    props.occupiedColumnCount = getTotalChildWidth({ id, components: dashboardLayout });
+    props.occupiedColumnCount = getTotalChildWidth({
+      id,
+      components: dashboardLayout,
+    });
   } else if (props.component.type === COLUMN_TYPE) {
     props.minColumnWidth = GRID_MIN_COLUMN_COUNT;
 
-    component.children.forEach((childId) => {
+    component.children.forEach(childId => {
       // rows don't have widths, so find the width of its children
       if (dashboardLayout[childId].type === ROW_TYPE) {
         props.minColumnWidth = Math.max(
@@ -51,23 +63,21 @@ function mapStateToProps({ dashboardLayout: undoableLayout, dashboardState: dash
         );
       }
     });
-  } else if (props.component.type === CHART_TYPE) {
-    const chartId = props.component.meta && props.component.meta.chartId;
-    if (chartId) {
-      props.chart = cells[chartId];
-    }
   }
 
   return props;
 }
 
 function mapDispatchToProps(dispatch) {
-  return bindActionCreators({
-    createComponent,
-    deleteComponent,
-    updateComponents,
-    handleComponentDrop,
-  }, dispatch);
+  return bindActionCreators(
+    {
+      createComponent,
+      deleteComponent,
+      updateComponents,
+      handleComponentDrop,
+    },
+    dispatch,
+  );
 }
 
 class DashboardComponent extends React.PureComponent {
diff --git a/superset/assets/src/dashboard/v2/containers/DashboardGrid.jsx b/superset/assets/src/dashboard/containers/DashboardGrid.jsx
similarity index 62%
rename from superset/assets/src/dashboard/v2/containers/DashboardGrid.jsx
rename to superset/assets/src/dashboard/containers/DashboardGrid.jsx
index 2adc390..718b543 100644
--- a/superset/assets/src/dashboard/v2/containers/DashboardGrid.jsx
+++ b/superset/assets/src/dashboard/containers/DashboardGrid.jsx
@@ -7,18 +7,20 @@ import {
   resizeComponent,
 } from '../actions/dashboardLayout';
 
-function mapStateToProps({ dashboardState: dashboard }, ownProps) {
+function mapStateToProps({ dashboardState }) {
   return {
-    editMode: dashboard.editMode,
-    cells: ownProps.cells,
+    editMode: dashboardState.editMode,
   };
 }
 
 function mapDispatchToProps(dispatch) {
-  return bindActionCreators({
-    handleComponentDrop,
-    resizeComponent,
-  }, dispatch);
+  return bindActionCreators(
+    {
+      handleComponentDrop,
+      resizeComponent,
+    },
+    dispatch,
+  );
 }
 
 export default connect(mapStateToProps, mapDispatchToProps)(DashboardGrid);
diff --git a/superset/assets/src/dashboard/v2/containers/DashboardHeader.jsx b/superset/assets/src/dashboard/containers/DashboardHeader.jsx
similarity index 59%
rename from superset/assets/src/dashboard/v2/containers/DashboardHeader.jsx
rename to superset/assets/src/dashboard/containers/DashboardHeader.jsx
index cc8e944..2b3431a 100644
--- a/superset/assets/src/dashboard/v2/containers/DashboardHeader.jsx
+++ b/superset/assets/src/dashboard/containers/DashboardHeader.jsx
@@ -2,7 +2,7 @@ import { ActionCreators as UndoActionCreators } from 'redux-undo';
 import { bindActionCreators } from 'redux';
 import { connect } from 'react-redux';
 
-import DashboardHeader from '../../components/Header';
+import DashboardHeader from '../components/Header';
 import {
   setEditMode,
   toggleBuilderPane,
@@ -13,13 +13,15 @@ import {
   updateDashboardTitle,
   onChange,
   onSave,
-} from '../../actions/dashboardState';
-import {
-  handleComponentDrop,
-} from '../actions/dashboardLayout';
+} from '../actions/dashboardState';
+import { handleComponentDrop } from '../actions/dashboardLayout';
 
-function mapStateToProps({ dashboardLayout: undoableLayout, dashboardState: dashboard,
-                           dashboardInfo, charts }) {
+function mapStateToProps({
+  dashboardLayout: undoableLayout,
+  dashboardState: dashboard,
+  dashboardInfo,
+  charts,
+}) {
   return {
     dashboardInfo,
     canUndo: undoableLayout.past.length > 0,
@@ -38,21 +40,23 @@ function mapStateToProps({ dashboardLayout: undoableLayout, dashboardState: dash
 }
 
 function mapDispatchToProps(dispatch) {
-  return bindActionCreators({
-    handleComponentDrop,
-    onUndo: UndoActionCreators.undo,
-    onRedo: UndoActionCreators.redo,
-    setEditMode,
-    toggleBuilderPane,
-    fetchFaveStar,
-    saveFaveStar,
-    fetchCharts,
-    startPeriodicRender,
-    updateDashboardTitle,
-    onChange,
-    onSave,
-  }, dispatch);
+  return bindActionCreators(
+    {
+      handleComponentDrop,
+      onUndo: UndoActionCreators.undo,
+      onRedo: UndoActionCreators.redo,
+      setEditMode,
+      toggleBuilderPane,
+      fetchFaveStar,
+      saveFaveStar,
+      fetchCharts,
+      startPeriodicRender,
+      updateDashboardTitle,
+      onChange,
+      onSave,
+    },
+    dispatch,
+  );
 }
 
-
 export default connect(mapStateToProps, mapDispatchToProps)(DashboardHeader);
diff --git a/superset/assets/src/dashboard/v2/containers/ToastPresenter.jsx b/superset/assets/src/dashboard/containers/ToastPresenter.jsx
similarity index 100%
rename from superset/assets/src/dashboard/v2/containers/ToastPresenter.jsx
rename to superset/assets/src/dashboard/containers/ToastPresenter.jsx
diff --git a/superset/assets/src/dashboard/v2/fixtures/emptyDashboardLayout.js b/superset/assets/src/dashboard/fixtures/emptyDashboardLayout.js
similarity index 92%
rename from superset/assets/src/dashboard/v2/fixtures/emptyDashboardLayout.js
rename to superset/assets/src/dashboard/fixtures/emptyDashboardLayout.js
index 7816cc2..cee948a 100644
--- a/superset/assets/src/dashboard/v2/fixtures/emptyDashboardLayout.js
+++ b/superset/assets/src/dashboard/fixtures/emptyDashboardLayout.js
@@ -14,9 +14,7 @@ export default {
   [DASHBOARD_ROOT_ID]: {
     type: DASHBOARD_ROOT_TYPE,
     id: DASHBOARD_ROOT_ID,
-    children: [
-      DASHBOARD_GRID_ID,
-    ],
+    children: [DASHBOARD_GRID_ID],
   },
 
   [DASHBOARD_GRID_ID]: {
diff --git a/superset/assets/src/dashboard/index.jsx b/superset/assets/src/dashboard/index.jsx
index 9c00f9e..846b82d 100644
--- a/superset/assets/src/dashboard/index.jsx
+++ b/superset/assets/src/dashboard/index.jsx
@@ -7,7 +7,7 @@ import thunk from 'redux-thunk';
 import { initEnhancer } from '../reduxUtils';
 import { appSetup } from '../common';
 import { initJQueryAjax } from '../modules/utils';
-import DashboardContainer from './components/DashboardContainer';
+import DashboardContainer from './containers/Dashboard';
 import getInitialState from './reducers/getInitialState';
 import rootReducer from './reducers/index';
 
@@ -19,7 +19,10 @@ const bootstrapData = JSON.parse(appContainer.getAttribute('data-bootstrap'));
 const initState = getInitialState(bootstrapData);
 
 const store = createStore(
-  rootReducer, initState, compose(applyMiddleware(thunk), initEnhancer(false)));
+  rootReducer,
+  initState,
+  compose(applyMiddleware(thunk), initEnhancer(false)),
+);
 
 ReactDOM.render(
   <Provider store={store}>
diff --git a/superset/assets/src/dashboard/v2/reducers/dashboardLayout.js b/superset/assets/src/dashboard/reducers/dashboardLayout.js
similarity index 76%
rename from superset/assets/src/dashboard/v2/reducers/dashboardLayout.js
rename to superset/assets/src/dashboard/reducers/dashboardLayout.js
index 994ac47..573a143 100644
--- a/superset/assets/src/dashboard/v2/reducers/dashboardLayout.js
+++ b/superset/assets/src/dashboard/reducers/dashboardLayout.js
@@ -1,4 +1,9 @@
-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';
@@ -23,7 +28,9 @@ import {
 
 const actionHandlers = {
   [UPDATE_COMPONENTS](state, action) {
-    const { payload: { nextComponents } } = action;
+    const {
+      payload: { nextComponents },
+    } = action;
     return {
       ...state,
       ...nextComponents,
@@ -31,7 +38,9 @@ const actionHandlers = {
   },
 
   [DELETE_COMPONENT](state, action) {
-    const { payload: { id, parentId } } = action;
+    const {
+      payload: { id, parentId },
+    } = action;
 
     if (!parentId || !id || !state[id] || !state[parentId]) return state;
 
@@ -44,10 +53,13 @@ const actionHandlers = {
       delete nextComponents[componentId];
 
       const { children = [] } = component;
-      children.forEach((childId) => { recursivelyDeleteChildren(childId, componentId); });
+      children.forEach(childId => {
+        recursivelyDeleteChildren(childId, componentId);
+      });
 
       const parent = nextComponents[componentParentId];
-      if (parent) { // may have been deleted in another recursion
+      if (parent) {
+        // may have been deleted in another recursion
         const componentIndex = (parent.children || []).indexOf(componentId);
         if (componentIndex > -1) {
           const nextChildren = [...parent.children];
@@ -66,21 +78,28 @@ const actionHandlers = {
   },
 
   [CREATE_COMPONENT](state, action) {
-    const { payload: { dropResult } } = action;
+    const {
+      payload: { dropResult },
+    } = action;
     const { destination, dragging } = dropResult;
     const newEntities = newEntitiesFromDrop({ dropResult, components: state });
 
-    // inherit the width of a column parent
-    if (destination.type === COLUMN_TYPE && [CHART_TYPE, MARKDOWN_TYPE].includes(dragging.type)) {
+    // 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];
+      const component = newEntitiesArray.find(
+        entity => entity.type === dragging.type,
+      );
 
       newEntities[component.id] = {
         ...component,
         meta: {
           ...component.meta,
-          width: parentColumn.meta.width,
+          width: GRID_MIN_COLUMN_COUNT,
         },
       };
     }
@@ -92,7 +111,9 @@ const actionHandlers = {
   },
 
   [MOVE_COMPONENT](state, action) {
-    const { payload: { dropResult } } = action;
+    const {
+      payload: { dropResult },
+    } = action;
     const { source, destination, dragging } = dropResult;
 
     if (!source || !destination || !dragging) return state;
@@ -119,7 +140,10 @@ const actionHandlers = {
     }
 
     // inherit the width of a column parent
-    if (destination.type === COLUMN_TYPE && [CHART_TYPE, MARKDOWN_TYPE].includes(dragging.type)) {
+    if (
+      destination.type === COLUMN_TYPE &&
+      [CHART_TYPE, MARKDOWN_TYPE].includes(dragging.type)
+    ) {
       const component = nextEntities[dragging.id];
       const parentColumn = nextEntities[destination.id];
       nextEntities[dragging.id] = {
@@ -138,7 +162,9 @@ const actionHandlers = {
   },
 
   [CREATE_TOP_LEVEL_TABS](state, action) {
-    const { payload: { dropResult } } = action;
+    const {
+      payload: { dropResult },
+    } = action;
     const { source, dragging } = dropResult;
 
     // move children of current root to be children of the dragging tab
@@ -153,7 +179,9 @@ const actionHandlers = {
       const draggingTab = state[draggingTabId];
 
       // move all children except the one that is dragging
-      const childrenToMove = [...topLevelComponent.children].filter(id => id !== dragging.id);
+      const childrenToMove = [...topLevelComponent.children].filter(
+        id => id !== dragging.id,
+      );
 
       return {
         ...state,
@@ -167,10 +195,7 @@ const actionHandlers = {
         },
         [draggingTabId]: {
           ...draggingTab,
-          children: [
-            ...draggingTab.children,
-            ...childrenToMove,
-          ],
+          children: [...draggingTab.children, ...childrenToMove],
         },
       };
     }
@@ -178,12 +203,19 @@ const actionHandlers = {
     // create new component
     const newEntities = newEntitiesFromDrop({ dropResult, components: state });
     const newEntitiesArray = Object.values(newEntities);
-    const tabComponent = newEntitiesArray.find(component => component.type === TAB_TYPE);
-    const tabsComponent = newEntitiesArray.find(component => component.type === TABS_TYPE);
+    const tabComponent = newEntitiesArray.find(
+      component => component.type === TAB_TYPE,
+    );
+    const tabsComponent = newEntitiesArray.find(
+      component => component.type === TABS_TYPE,
+    );
 
     tabComponent.children = [...topLevelComponent.children];
     newEntities[topLevelId] = { ...topLevelComponent, children: [] };
-    newEntities[DASHBOARD_ROOT_ID] = { ...rootComponent, children: [tabsComponent.id] };
+    newEntities[DASHBOARD_ROOT_ID] = {
+      ...rootComponent,
+      children: [tabsComponent.id],
+    };
 
     return {
       ...state,
@@ -201,7 +233,7 @@ const actionHandlers = {
     let childrenToMove = [];
     const nextEntities = { ...state };
 
-    topLevelTabs.children.forEach((tabId) => {
+    topLevelTabs.children.forEach(tabId => {
       const tabComponent = state[tabId];
       childrenToMove = [...childrenToMove, ...tabComponent.children];
       delete nextEntities[tabId];
@@ -215,7 +247,7 @@ const actionHandlers = {
     };
 
     nextEntities[DASHBOARD_GRID_ID] = {
-      ...(state[DASHBOARD_GRID_ID]),
+      ...state[DASHBOARD_GRID_ID],
       children: childrenToMove,
     };
 
diff --git a/superset/assets/src/dashboard/reducers/dashboardState.js b/superset/assets/src/dashboard/reducers/dashboardState.js
index 84ee58e..7b5a17a 100644
--- a/superset/assets/src/dashboard/reducers/dashboardState.js
+++ b/superset/assets/src/dashboard/reducers/dashboardState.js
@@ -9,13 +9,14 @@ import {
   REMOVE_SLICE,
   REMOVE_FILTER,
   SET_EDIT_MODE,
+  SET_UNSAVED_CHANGES,
   TOGGLE_BUILDER_PANE,
   TOGGLE_EXPAND_SLICE,
   TOGGLE_FAVE_STAR,
   UPDATE_DASHBOARD_TITLE,
 } from '../actions/dashboardState';
 
-export default function (state = {}, action) {
+export default function dashboardStateReducer(state = {}, action) {
   const actionHandlers = {
     [UPDATE_DASHBOARD_TITLE]() {
       return { ...state, title: action.title };
@@ -84,15 +85,23 @@ export default function (state = {}, action) {
       let filters = state.filters;
       const { chart, col, vals, merge, refresh } = action;
       const sliceId = chart.id;
-      const filterKeys = ['__from', '__to', '__time_col',
-        '__time_grain', '__time_origin', '__granularity'];
-      if (filterKeys.indexOf(col) >= 0 ||
-        action.chart.formData.groupby.indexOf(col) !== -1) {
+      const filterKeys = [
+        '__from',
+        '__to',
+        '__time_col',
+        '__time_grain',
+        '__time_origin',
+        '__granularity',
+      ];
+      if (
+        filterKeys.indexOf(col) >= 0 ||
+        action.chart.formData.groupby.indexOf(col) !== -1
+      ) {
         let newFilter = {};
         if (!(sliceId in filters)) {
           // Straight up set the filters if none existed for the slice
           newFilter = { [col]: vals };
-        } else if (filters[sliceId] && !(col in filters[sliceId]) || !merge) {
+        } else if ((filters[sliceId] && !(col in filters[sliceId])) || !merge) {
           newFilter = { ...filters[sliceId], [col]: vals };
           // d3.merge pass in array of arrays while some value form filter components
           // from and to filter box require string to be process and return
@@ -119,6 +128,10 @@ export default function (state = {}, action) {
       }
       return { ...state, filters, refresh };
     },
+    [SET_UNSAVED_CHANGES]() {
+      const { hasUnsavedChanges } = action.payload;
+      return { ...state, hasUnsavedChanges };
+    },
   };
 
   if (action.type in actionHandlers) {
diff --git a/superset/assets/src/dashboard/reducers/datasources.js b/superset/assets/src/dashboard/reducers/datasources.js
index 4df7507..87f6d09 100644
--- a/superset/assets/src/dashboard/reducers/datasources.js
+++ b/superset/assets/src/dashboard/reducers/datasources.js
@@ -1,8 +1,8 @@
-import * as actions from '../actions/datasources';
+import { SET_DATASOURCE } from '../actions/datasources';
 
 export default function datasourceReducer(datasources = {}, action) {
   const actionHandlers = {
-    [actions.SET_DATASOURCE]() {
+    [SET_DATASOURCE]() {
       return action.datasource;
     },
   };
@@ -10,7 +10,10 @@ export default function datasourceReducer(datasources = {}, action) {
   if (action.type in actionHandlers) {
     return {
       ...datasources,
-      [action.key]: actionHandlers[action.type](datasources[action.key], action),
+      [action.key]: actionHandlers[action.type](
+        datasources[action.key],
+        action,
+      ),
     };
   }
   return datasources;
diff --git a/superset/assets/src/dashboard/reducers/getInitialState.js b/superset/assets/src/dashboard/reducers/getInitialState.js
index 1129210..d0b4d7b 100644
--- a/superset/assets/src/dashboard/reducers/getInitialState.js
+++ b/superset/assets/src/dashboard/reducers/getInitialState.js
@@ -7,9 +7,9 @@ import { getParam } from '../../modules/utils';
 import { applyDefaultFormData } from '../../explore/stores/store';
 import { getColorFromScheme } from '../../modules/colors';
 import layoutConverter from '../util/dashboardLayoutConverter';
-import { DASHBOARD_ROOT_ID } from '../v2/util/constants';
+import { DASHBOARD_ROOT_ID } from '../util/constants';
 
-export default function (bootstrapData) {
+export default function(bootstrapData) {
   const { user_id, datasources, common } = bootstrapData;
   delete common.locale;
   delete common.language_pack;
@@ -18,7 +18,9 @@ export default function (bootstrapData) {
   let filters = {};
   try {
     // allow request parameter overwrite dashboard metadata
-    filters = JSON.parse(getParam('preselect_filters') || dashboard.metadata.default_filters);
+    filters = JSON.parse(
+      getParam('preselect_filters') || dashboard.metadata.default_filters,
+    );
   } catch (e) {
     //
   }
@@ -27,9 +29,9 @@ export default function (bootstrapData) {
   // the dashboard's JSON metadata
   if (dashboard.metadata && dashboard.metadata.label_colors) {
     const colorMap = dashboard.metadata.label_colors;
-    for (const label in colorMap) {
+    Object.keys(colorMap).forEach(label => {
       getColorFromScheme(label, null, colorMap[label]);
-    }
+    });
   }
 
   // dashboard layout
@@ -52,9 +54,10 @@ export default function (bootstrapData) {
   const chartQueries = {};
   const slices = {};
   const sliceIds = new Set();
-  dashboard.slices.forEach((slice) => {
+  dashboard.slices.forEach(slice => {
     const key = slice.slice_id;
-    chartQueries[key] = { ...chart,
+    chartQueries[key] = {
+      ...chart,
       id: key,
       form_data: slice.form_data,
       formData: applyDefaultFormData(slice.form_data),
@@ -79,13 +82,16 @@ export default function (bootstrapData) {
     datasources,
     sliceEntities: { ...initSliceEntities, slices, isLoading: false },
     charts: chartQueries,
-    dashboardInfo: {  /* readOnly props */
+    dashboardInfo: {
+      // read-only data
       id: dashboard.id,
       slug: dashboard.slug,
       metadata: {
-        filter_immune_slice_fields: dashboard.metadata.filter_immune_slice_fields,
+        filter_immune_slice_fields:
+          dashboard.metadata.filter_immune_slice_fields,
         filter_immune_slices: dashboard.metadata.filter_immune_slices,
-        timed_refresh_immune_slices: dashboard.metadata.timed_refresh_immune_slices,
+        timed_refresh_immune_slices:
+          dashboard.metadata.timed_refresh_immune_slices,
       },
       userId: user_id,
       dash_edit_perm: dashboard.dash_edit_perm,
diff --git a/superset/assets/src/dashboard/reducers/index.js b/superset/assets/src/dashboard/reducers/index.js
index a2397e0..787cd5f 100644
--- a/superset/assets/src/dashboard/reducers/index.js
+++ b/superset/assets/src/dashboard/reducers/index.js
@@ -4,19 +4,19 @@ import charts from '../../chart/chartReducer';
 import dashboardState from './dashboardState';
 import datasources from './datasources';
 import sliceEntities from './sliceEntities';
-import dashboardLayout from '../v2/reducers/index';
-import messageToasts from '../v2/reducers/messageToasts';
+import dashboardLayout from '../reducers/undoableDashboardLayout';
+import messageToasts from '../reducers/messageToasts';
 
-const dashboardInfo = (state = {}) => (state);
-const impressionId = (state = '') => (state);
+const dashboardInfo = (state = {}) => state;
+const impressionId = (state = '') => state;
 
 export default combineReducers({
   charts,
   datasources,
-  sliceEntities,
   dashboardInfo,
   dashboardState,
   dashboardLayout,
-  messageToasts,
   impressionId,
+  messageToasts,
+  sliceEntities,
 });
diff --git a/superset/assets/src/dashboard/v2/reducers/messageToasts.js b/superset/assets/src/dashboard/reducers/messageToasts.js
similarity index 87%
rename from superset/assets/src/dashboard/v2/reducers/messageToasts.js
rename to superset/assets/src/dashboard/reducers/messageToasts.js
index 1f5728a..7383ab0 100644
--- a/superset/assets/src/dashboard/v2/reducers/messageToasts.js
+++ b/superset/assets/src/dashboard/reducers/messageToasts.js
@@ -8,7 +8,9 @@ export default function messageToastsReducer(toasts = [], action) {
     }
 
     case REMOVE_TOAST: {
-      const { payload: { id } } = action;
+      const {
+        payload: { id },
+      } = action;
       return [...toasts].filter(toast => toast.id !== id);
     }
 
diff --git a/superset/assets/src/dashboard/reducers/sliceEntities.js b/superset/assets/src/dashboard/reducers/sliceEntities.js
index 61a58f6..c1453f5 100644
--- a/superset/assets/src/dashboard/reducers/sliceEntities.js
+++ b/superset/assets/src/dashboard/reducers/sliceEntities.js
@@ -13,7 +13,10 @@ export const initSliceEntities = {
   lastUpdated: 0,
 };
 
-export default function (state = initSliceEntities, action) {
+export default function sliceEntitiesReducer(
+  state = initSliceEntities,
+  action,
+) {
   const actionHandlers = {
     [UPDATE_SLICE_NAME]() {
       const updatedSlice = {
@@ -44,8 +47,9 @@ export default function (state = initSliceEntities, action) {
       const respJSON = action.error.responseJSON;
       const errorMessage =
         t('Sorry, there was an error adding slices to this dashboard: ') +
-        (respJSON && respJSON.message) ? respJSON.message :
-          error.responseText;
+        (respJSON && respJSON.message)
+          ? respJSON.message
+          : action.error.responseText;
       return {
         ...state,
         isLoading: false,
diff --git a/superset/assets/src/dashboard/reducers/undoableDashboardLayout.js b/superset/assets/src/dashboard/reducers/undoableDashboardLayout.js
new file mode 100644
index 0000000..b78c273
--- /dev/null
+++ b/superset/assets/src/dashboard/reducers/undoableDashboardLayout.js
@@ -0,0 +1,27 @@
+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 default undoable(dashboardLayout, {
+  limit: 15,
+  filter: includeAction([
+    UPDATE_COMPONENTS,
+    DELETE_COMPONENT,
+    CREATE_COMPONENT,
+    CREATE_TOP_LEVEL_TABS,
+    DELETE_TOP_LEVEL_TABS,
+    RESIZE_COMPONENT,
+    MOVE_COMPONENT,
+    HANDLE_COMPONENT_DROP,
+  ]),
+});
diff --git a/superset/assets/src/dashboard/v2/stylesheets/builder-sidepane.less b/superset/assets/src/dashboard/stylesheets/builder-sidepane.less
similarity index 76%
rename from superset/assets/src/dashboard/v2/stylesheets/builder-sidepane.less
rename to superset/assets/src/dashboard/stylesheets/builder-sidepane.less
index d9f1069..bdf342b 100644
--- a/superset/assets/src/dashboard/v2/stylesheets/builder-sidepane.less
+++ b/superset/assets/src/dashboard/stylesheets/builder-sidepane.less
@@ -1,8 +1,21 @@
 .dashboard-builder-sidepane {
+  background: white;
+  flex: 0 0 376px;
+  border: 1px solid @gray-light;
+  z-index: 10;
+  position: relative;
+
+  .dashboard-builder-sidepane-header {
+    font-size: 15px;
+    font-weight: 700;
+    border-bottom: 1px solid @gray-light;
+    padding: 14px;
+  }
+
   .trigger {
     height: 25px;
     width: 25px;
-    color: #879399;
+    color: @gray;
     position: relative;
 
     &.close {
@@ -25,8 +38,8 @@
     position: absolute;
     width: 2px;
     top: 51px;
-    right: 1px;
-    background: #fff;
+    right: 0;
+    background: white;
     transition-property: width;
     transition-duration: 1s;
     transition-timing-function: ease;
@@ -39,18 +52,17 @@
 
   .chart-card-container {
     padding: 16px;
-    cursor: move;
 
     .chart-card {
-      border: 1px solid #ccc;
+      border: 1px solid @gray-light;
       height: 120px;
       padding: 16px;
-      pointer-events: unset;
+      cursor: move;
     }
 
     .chart-card.is-selected {
       opacity: 0.45;
-      pointer-events: none;
+      cursor: not-allowed;
     }
 
     .card-title {
@@ -88,7 +100,7 @@
       input {
         margin-left: 16px;
         width: 169px;
-        border: 1px solid #b3b3b3;
+        border: 1px solid @gray;
 
         &:focus {
           outline: none;
diff --git a/superset/assets/src/dashboard/v2/stylesheets/builder.less b/superset/assets/src/dashboard/stylesheets/builder.less
similarity index 72%
rename from superset/assets/src/dashboard/v2/stylesheets/builder.less
rename to superset/assets/src/dashboard/stylesheets/builder.less
index 2ff99a4..7c14056 100644
--- a/superset/assets/src/dashboard/v2/stylesheets/builder.less
+++ b/superset/assets/src/dashboard/stylesheets/builder.less
@@ -1,7 +1,7 @@
-.dashboard-v2 {
-  //margin-top: -20px;
+.dashboard {
   position: relative;
   color: @almost-black;
+  margin-top: -20px;
 }
 
 .dashboard-header {
@@ -22,12 +22,12 @@
 }
 
 /* only top-level tabs have popover, give it more padding to match header + tabs */
-.dashboard-v2 > .with-popover-menu > .popover-menu {
+.dashboard > .with-popover-menu > .popover-menu {
   left: 24px;
 }
 
 /* drop shadow for top-level tabs only */
-.dashboard-v2 .dashboard-component-tabs {
+.dashboard .dashboard-component-tabs {
   box-shadow: 0 4px 4px 0 rgba(0, 0, 0, 0.1);
   padding-left: 8px; /* note this is added to tab-level padding, to match header */
 }
@@ -43,21 +43,6 @@
   position: relative;
 }
 
-.dashboard-builder-sidepane {
-  background: white;
-  flex: 0 0 376px;
-  border: 1px solid @gray-light;
-  z-index: 1;
-  position: relative;
-}
-
-.dashboard-builder-sidepane-header {
-  font-size: 15px;
-  font-weight: 700;
-  border-bottom: 1px solid @gray-light;
-  padding: 14px;
-}
-
 /* @TODO remove upon new theme */
 .btn.btn-primary {
   background: @almost-black !important;
diff --git a/superset/assets/src/dashboard/v2/stylesheets/buttons.less b/superset/assets/src/dashboard/stylesheets/buttons.less
similarity index 100%
rename from superset/assets/src/dashboard/v2/stylesheets/buttons.less
rename to superset/assets/src/dashboard/stylesheets/buttons.less
diff --git a/superset/assets/src/dashboard/stylesheets/components/chart.less b/superset/assets/src/dashboard/stylesheets/components/chart.less
new file mode 100644
index 0000000..dc366a1
--- /dev/null
+++ b/superset/assets/src/dashboard/stylesheets/components/chart.less
@@ -0,0 +1,69 @@
+.dashboard-component-chart-holder {
+  width: 100%;
+  height: 100%;
+  color: @gray-dark;
+  background-color: white;
+  position: relative;
+  padding: 16px;
+}
+
+.dashboard-chart {
+  overflow: hidden;
+}
+
+.dashboard-chart.dashboard-chart--overflowable {
+  overflow: visible;
+}
+
+.dashboard--editing .dashboard-component-chart-holder:after {
+  content: '';
+  position: absolute;
+  width: 100%;
+  height: 100%;
+  top: 0px;
+  left: 0px;
+  z-index: 1;
+  pointer-events: none;
+  border: 1px solid transparent;
+}
+
+.dashboard--editing
+  .resizable-container:hover
+  > .dashboard-component-chart-holder:after,
+.dashboard--editing .dashboard-component-chart-holder:hover:after {
+  border: 1px solid @gray-light;
+}
+
+.dashboard--editing
+  .resizable-container.resizable-container--resizing:hover
+  > .dashboard-component-chart-holder:after {
+  border: 1px solid @indicator-color;
+}
+
+.dashboard--editing
+  .dashboard-component-chart-holder
+  .dashboard-chart
+  .chart-container {
+  cursor: move;
+  opacity: 0.2;
+}
+
+.dashboard--editing
+  .dashboard-component-chart-holder:hover
+  .dashboard-chart
+  .chart-container {
+  opacity: 0.7;
+}
+
+.dashboard--editing
+  .dashboard-component-chart-holder
+  .dashboard-chart
+  .slice_container {
+  /* disable chart interactions in edit mode */
+  pointer-events: none;
+}
+
+.dashboard-chart .chart-header {
+  font-size: 16px;
+  font-weight: bold;
+}
diff --git a/superset/assets/src/dashboard/v2/stylesheets/components/column.less b/superset/assets/src/dashboard/stylesheets/components/column.less
similarity index 60%
rename from superset/assets/src/dashboard/v2/stylesheets/components/column.less
rename to superset/assets/src/dashboard/stylesheets/components/column.less
index 9565112..5fcb442 100644
--- a/superset/assets/src/dashboard/v2/stylesheets/components/column.less
+++ b/superset/assets/src/dashboard/stylesheets/components/column.less
@@ -1,5 +1,6 @@
 .grid-column {
   width: 100%;
+  position: relative;
 }
 
 /* gutters between elements in a column */
@@ -7,20 +8,24 @@
   margin-bottom: 16px;
 }
 
-.dashboard-v2--editing .grid-column:after {
-  border: 1px dashed transparent;
-  content: "";
+.dashboard--editing .grid-column:after {
+  border: 1px solid transparent;
+  content: '';
   position: absolute;
   width: 100%;
   height: 100%;
-  top: 1px;
+  top: 0;
   left: 0;
   z-index: 1;
   pointer-events: none;
 }
 
-.dashboard-v2--editing .grid-column:hover:after {
-  border: 1px solid @gray-light;
+.dashboard--editing
+  .resizable-container.resizable-container--resizing:hover
+  > .grid-column:after,
+.dashboard--editing .grid-column:hover:after {
+  border: 1px dashed @gray-light;
+  box-shadow: 0 4px 4px 0 rgba(0, 0, 0, 0.1);
 }
 
 .grid-column > .hover-menu--top {
@@ -32,7 +37,7 @@
 }
 
 .grid-column--empty:before {
-  content: "Empty column";
+  content: 'Empty column';
   position: absolute;
   top: 0;
   left: 0;
diff --git a/superset/assets/src/dashboard/v2/stylesheets/components/divider.less b/superset/assets/src/dashboard/stylesheets/components/divider.less
similarity index 100%
rename from superset/assets/src/dashboard/v2/stylesheets/components/divider.less
rename to superset/assets/src/dashboard/stylesheets/components/divider.less
diff --git a/superset/assets/src/dashboard/v2/stylesheets/components/header.less b/superset/assets/src/dashboard/stylesheets/components/header.less
similarity index 92%
rename from superset/assets/src/dashboard/v2/stylesheets/components/header.less
rename to superset/assets/src/dashboard/stylesheets/components/header.less
index 37c7598..8b93164 100644
--- a/superset/assets/src/dashboard/v2/stylesheets/components/header.less
+++ b/superset/assets/src/dashboard/stylesheets/components/header.less
@@ -11,6 +11,10 @@
   width: auto;
 }
 
+.dashboard-header .btn-group button {
+  margin-right: 8px;
+}
+
 .dragdroppable-row .dashboard-component-header {
   cursor: move;
 }
@@ -40,7 +44,6 @@
  * grids add margin between items, so don't double pad within columns
  * we'll not worry about double padding on top as it can serve as a visual separator
  */
-// .grid-content > :not(:only-child):not(:last-child) .dashboard-component-header,
 .grid-column > :not(:only-child):not(:last-child) .dashboard-component-header {
   margin-bottom: -16px;
 }
diff --git a/superset/assets/src/dashboard/v2/stylesheets/components/index.less b/superset/assets/src/dashboard/stylesheets/components/index.less
similarity index 100%
rename from superset/assets/src/dashboard/v2/stylesheets/components/index.less
rename to superset/assets/src/dashboard/stylesheets/components/index.less
diff --git a/superset/assets/src/dashboard/v2/stylesheets/components/new-component.less b/superset/assets/src/dashboard/stylesheets/components/new-component.less
similarity index 100%
rename from superset/assets/src/dashboard/v2/stylesheets/components/new-component.less
rename to superset/assets/src/dashboard/stylesheets/components/new-component.less
diff --git a/superset/assets/src/dashboard/v2/stylesheets/components/row.less b/superset/assets/src/dashboard/stylesheets/components/row.less
similarity index 66%
rename from superset/assets/src/dashboard/v2/stylesheets/components/row.less
rename to superset/assets/src/dashboard/stylesheets/components/row.less
index 956966d..7df5675 100644
--- a/superset/assets/src/dashboard/v2/stylesheets/components/row.less
+++ b/superset/assets/src/dashboard/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;
@@ -13,20 +14,24 @@
 }
 
 /* hover indicator */
-.dashboard-v2--editing .grid-row:after {
+.dashboard--editing .grid-row:after {
   border: 1px dashed transparent;
-  content: "";
+  content: '';
   position: absolute;
   width: 100%;
   height: 100%;
-  top: 1px;
+  top: 0;
   left: 0;
   z-index: 1;
   pointer-events: none;
 }
 
-.dashboard-v2--editing .grid-row:hover:after {
-  border: 1px solid @gray-light;
+.dashboard--editing
+  .resizable-container.resizable-container--resizing:hover
+  > .grid-row:after,
+.dashboard--editing .grid-row:hover:after {
+  border: 1px dashed @gray-light;
+  box-shadow: 0 4px 4px 0 rgba(0, 0, 0, 0.1);
 }
 
 .grid-row.grid-row--empty {
@@ -38,7 +43,7 @@
   position: absolute;
   top: 0;
   left: 0;
-  content: "Empty row";
+  content: 'Empty row';
   display: flex;
   align-items: center;
   justify-content: center;
diff --git a/superset/assets/src/dashboard/v2/stylesheets/components/tabs.less b/superset/assets/src/dashboard/stylesheets/components/tabs.less
similarity index 100%
rename from superset/assets/src/dashboard/v2/stylesheets/components/tabs.less
rename to superset/assets/src/dashboard/stylesheets/components/tabs.less
diff --git a/superset/assets/src/dashboard/stylesheets/dashboard.less b/superset/assets/src/dashboard/stylesheets/dashboard.less
new file mode 100644
index 0000000..03c804b
--- /dev/null
+++ b/superset/assets/src/dashboard/stylesheets/dashboard.less
@@ -0,0 +1,104 @@
+// @import './less/cosmo/variables.less';
+
+.dashboard .chart-header {
+  position: relative;
+
+  .dropdown.btn-group {
+    position: absolute;
+    right: 0;
+  }
+
+  .dropdown-menu.dropdown-menu-right {
+    right: 7px;
+    top: -3px;
+  }
+}
+
+.slice-header-controls-trigger {
+  border: 0;
+  padding: 0 0 0 20px;
+  background: none;
+  outline: none;
+  box-shadow: none;
+  color: #263238;
+
+  &.is-cached {
+    color: red;
+  }
+
+  &:hover,
+  &:focus {
+    background: none;
+    cursor: pointer;
+  }
+
+  .controls-container.dropdown-menu {
+    top: 0;
+    left: unset;
+    right: 10px;
+
+    &.is-open {
+      display: block;
+    }
+
+    & li {
+      white-space: nowrap;
+    }
+  }
+}
+
+.modal img.loading {
+  width: 50px;
+  margin: 0;
+  position: relative;
+}
+
+.react-bs-container-body {
+  max-height: 400px;
+  overflow-y: auto;
+}
+
+.hidden,
+#pageDropDown {
+  display: none;
+}
+
+.separator .chart-container {
+  position: absolute;
+  left: 0;
+  right: 0;
+  top: 0;
+  bottom: 0;
+}
+
+.dashboard .title {
+  margin: 0 20px;
+}
+
+.dashboard .title .favstar {
+  font-size: 20px;
+  line-height: 1em;
+  position: relative;
+  top: -5px;
+}
+
+.ace_gutter {
+  z-index: 0;
+}
+.ace_content {
+  z-index: 0;
+}
+.ace_scrollbar {
+  z-index: 0;
+}
+.slice_container .alert {
+  margin: 10px;
+}
+
+i.danger {
+  color: red;
+}
+
+i.warning {
+  color: orange;
+}
diff --git a/superset/assets/src/dashboard/v2/stylesheets/dnd.less b/superset/assets/src/dashboard/stylesheets/dnd.less
similarity index 98%
rename from superset/assets/src/dashboard/v2/stylesheets/dnd.less
rename to superset/assets/src/dashboard/stylesheets/dnd.less
index 45a9784..835b62b 100644
--- a/superset/assets/src/dashboard/v2/stylesheets/dnd.less
+++ b/superset/assets/src/dashboard/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/src/dashboard/v2/stylesheets/grid.less b/superset/assets/src/dashboard/stylesheets/grid.less
similarity index 68%
rename from superset/assets/src/dashboard/v2/stylesheets/grid.less
rename to superset/assets/src/dashboard/stylesheets/grid.less
index 45b8a42..a12ac97 100644
--- a/superset/assets/src/dashboard/v2/stylesheets/grid.less
+++ b/superset/assets/src/dashboard/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/src/dashboard/v2/stylesheets/hover-menu.less b/superset/assets/src/dashboard/stylesheets/hover-menu.less
similarity index 100%
rename from superset/assets/src/dashboard/v2/stylesheets/hover-menu.less
rename to superset/assets/src/dashboard/stylesheets/hover-menu.less
diff --git a/superset/assets/src/dashboard/v2/stylesheets/index.less b/superset/assets/src/dashboard/stylesheets/index.less
similarity index 81%
rename from superset/assets/src/dashboard/v2/stylesheets/index.less
rename to superset/assets/src/dashboard/stylesheets/index.less
index 49ff5da..b69c7b0 100644
--- a/superset/assets/src/dashboard/v2/stylesheets/index.less
+++ b/superset/assets/src/dashboard/stylesheets/index.less
@@ -1,7 +1,9 @@
 @import './variables.less';
 
 @import './builder.less';
+@import './builder-sidepane.less';
 @import './buttons.less';
+@import './dashboard.less';
 @import './dnd.less';
 @import './grid.less';
 @import './hover-menu.less';
diff --git a/superset/assets/src/dashboard/v2/stylesheets/popover-menu.less b/superset/assets/src/dashboard/stylesheets/popover-menu.less
similarity index 100%
rename from superset/assets/src/dashboard/v2/stylesheets/popover-menu.less
rename to superset/assets/src/dashboard/stylesheets/popover-menu.less
diff --git a/superset/assets/src/dashboard/v2/stylesheets/resizable.less b/superset/assets/src/dashboard/stylesheets/resizable.less
similarity index 69%
rename from superset/assets/src/dashboard/v2/stylesheets/resizable.less
rename to superset/assets/src/dashboard/stylesheets/resizable.less
index 7bdd5f8..973daab 100644
--- a/superset/assets/src/dashboard/v2/stylesheets/resizable.less
+++ b/superset/assets/src/dashboard/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/src/dashboard/v2/stylesheets/toast.less b/superset/assets/src/dashboard/stylesheets/toast.less
similarity index 91%
rename from superset/assets/src/dashboard/v2/stylesheets/toast.less
rename to superset/assets/src/dashboard/stylesheets/toast.less
index a508637..1d1ebc5 100644
--- a/superset/assets/src/dashboard/v2/stylesheets/toast.less
+++ b/superset/assets/src/dashboard/stylesheets/toast.less
@@ -13,8 +13,7 @@
   opacity: 0;
   position: relative;
   white-space: pre-line;
-  box-shadow: 0 2px 4px 0 rgba(0, 0, 0, 0.15);
-  border-radius: 2px;
+  box-shadow: 0 2px 4px 0 rgba(0, 0, 0, 0.35);
   will-change: transform, opacity;
   transform: translateY(-100%);
   transition: transform .3s, opacity .3s;
@@ -38,7 +37,7 @@
   position: absolute;
   top: 0;
   left: 0;
-  width: 4px;
+  width: 6px;
   height: 100%;
 }
 
diff --git a/superset/assets/src/dashboard/v2/stylesheets/variables.less b/superset/assets/src/dashboard/stylesheets/variables.less
similarity index 100%
rename from superset/assets/src/dashboard/v2/stylesheets/variables.less
rename to superset/assets/src/dashboard/stylesheets/variables.less
diff --git a/superset/assets/src/dashboard/util/backgroundStyleOptions.js b/superset/assets/src/dashboard/util/backgroundStyleOptions.js
new file mode 100644
index 0000000..926e7f1
--- /dev/null
+++ b/superset/assets/src/dashboard/util/backgroundStyleOptions.js
@@ -0,0 +1,15 @@
+import { t } from '../../locales';
+import { BACKGROUND_TRANSPARENT, BACKGROUND_WHITE } from './constants';
+
+export default [
+  {
+    value: BACKGROUND_TRANSPARENT,
+    label: t('Transparent'),
+    className: 'background--transparent',
+  },
+  {
+    value: BACKGROUND_WHITE,
+    label: t('White'),
+    className: 'background--white',
+  },
+];
diff --git a/superset/assets/src/dashboard/util/charts/getEffectiveExtraFilters.js b/superset/assets/src/dashboard/util/charts/getEffectiveExtraFilters.js
new file mode 100644
index 0000000..f48631f
--- /dev/null
+++ b/superset/assets/src/dashboard/util/charts/getEffectiveExtraFilters.js
@@ -0,0 +1,42 @@
+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/src/dashboard/util/charts/getFormDataWithExtraFilters.js b/superset/assets/src/dashboard/util/charts/getFormDataWithExtraFilters.js
new file mode 100644
index 0000000..031d90d
--- /dev/null
+++ b/superset/assets/src/dashboard/util/charts/getFormDataWithExtraFilters.js
@@ -0,0 +1,42 @@
+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
+const cachedDashboardMetadataByChart = {};
+const cachedFiltersByChart = {};
+const cachedFormdataByChart = {};
+
+export default function getFormDataWithExtraFilters({
+  chart,
+  dashboardMetadata,
+  filters,
+  sliceId,
+}) {
+  // if dashboard metadata + filters have not changed, use cache if possible
+  if (
+    cachedDashboardMetadataByChart[sliceId] &&
+    cachedDashboardMetadataByChart[sliceId] === dashboardMetadata &&
+    cachedFiltersByChart[sliceId] &&
+    cachedFiltersByChart[sliceId] === filters &&
+    cachedFormdataByChart[sliceId]
+  ) {
+    return cachedFormdataByChart[sliceId];
+  }
+
+  const extraFilters = getEffectiveExtraFilters({
+    dashboardMetadata,
+    filters,
+    sliceId,
+  });
+
+  const formData = {
+    ...chart.formData,
+    extra_filters: [...chart.formData.filters, ...extraFilters],
+  };
+
+  cachedDashboardMetadataByChart[sliceId] = dashboardMetadata;
+  cachedFiltersByChart[sliceId] = filters;
+  cachedFormdataByChart[sliceId] = formData;
+
+  return formData;
+}
diff --git a/superset/assets/src/dashboard/util/componentIsResizable.js b/superset/assets/src/dashboard/util/componentIsResizable.js
new file mode 100644
index 0000000..45812d7
--- /dev/null
+++ b/superset/assets/src/dashboard/util/componentIsResizable.js
@@ -0,0 +1,5 @@
+import { COLUMN_TYPE, CHART_TYPE, MARKDOWN_TYPE } from './componentTypes';
+
+export default function componentIsResizable(entity) {
+  return [COLUMN_TYPE, CHART_TYPE, MARKDOWN_TYPE].indexOf(entity.type) > -1;
+}
diff --git a/superset/assets/src/dashboard/v2/util/componentTypes.js b/superset/assets/src/dashboard/util/componentTypes.js
similarity index 100%
rename from superset/assets/src/dashboard/v2/util/componentTypes.js
rename to superset/assets/src/dashboard/util/componentTypes.js
diff --git a/superset/assets/src/dashboard/v2/util/constants.js b/superset/assets/src/dashboard/util/constants.js
similarity index 100%
rename from superset/assets/src/dashboard/v2/util/constants.js
rename to superset/assets/src/dashboard/util/constants.js
diff --git a/superset/assets/src/dashboard/util/dashboardHelper.js b/superset/assets/src/dashboard/util/dashboardHelper.js
deleted file mode 100644
index c9a6021..0000000
--- a/superset/assets/src/dashboard/util/dashboardHelper.js
+++ /dev/null
@@ -1,9 +0,0 @@
-export function getChartIdsFromLayout(layout) {
-  return Object.values(layout)
-    .reduce((chartIds, value) => {
-      if (value && value.meta && value.meta.chartId) {
-        chartIds.push(value.meta.chartId);
-      }
-      return chartIds;
-    }, []);
-}
diff --git a/superset/assets/src/dashboard/util/dashboardLayoutConverter.js b/superset/assets/src/dashboard/util/dashboardLayoutConverter.js
index 854ca65..f04b50e 100644
--- a/superset/assets/src/dashboard/util/dashboardLayoutConverter.js
+++ b/superset/assets/src/dashboard/util/dashboardLayoutConverter.js
@@ -8,19 +8,23 @@ import {
   DASHBOARD_HEADER_TYPE,
   DASHBOARD_ROOT_TYPE,
   DASHBOARD_GRID_TYPE,
-} from '../v2/util/componentTypes';
+} from './componentTypes';
 import {
   DASHBOARD_GRID_ID,
   DASHBOARD_HEADER_ID,
   DASHBOARD_ROOT_ID,
-} from '../v2/util/constants';
+} from './constants';
 
 const MAX_RECURSIVE_LEVEL = 6;
 const GRID_RATIO = 4;
 const ROW_HEIGHT = 8;
 const generateId = (() => {
   let componentId = 1;
-  return () => (componentId++);
+  return () => {
+    const id = componentId;
+    componentId += 1;
+    return id;
+  };
 })();
 
 /**
@@ -33,7 +37,7 @@ function getBoundary(positions) {
   let bottom = 0;
   let left = Number.MAX_VALUE;
   let right = 1;
-  positions.forEach((item) => {
+  positions.forEach(item => {
     const { row, col, size_x, size_y } = item;
     if (row <= top) top = row;
     if (col <= left) left = col;
@@ -50,11 +54,10 @@ function getBoundary(positions) {
 }
 
 function getRowContainer() {
-  const id = 'DASHBOARD_ROW_TYPE-' + generateId();
   return {
     version: 'v2',
     type: ROW_TYPE,
-    id,
+    id: `DASHBOARD_ROW_TYPE-${generateId()}`,
     children: [],
     meta: {
       background: 'BACKGROUND_TRANSPARENT',
@@ -63,11 +66,10 @@ function getRowContainer() {
 }
 
 function getColContainer() {
-  const id = 'DASHBOARD_COLUMN_TYPE-' + generateId();
   return {
     version: 'v2',
     type: COLUMN_TYPE,
-    id,
+    id: `DASHBOARD_COLUMN_TYPE-${generateId()}`,
     children: [],
     meta: {
       background: 'BACKGROUND_TRANSPARENT',
@@ -88,7 +90,7 @@ function getChartHolder(item) {
   return {
     version: 'v2',
     type: CHART_TYPE,
-    id: 'DASHBOARD_CHART_TYPE-' + generateId(),
+    id: `DASHBOARD_CHART_TYPE-${generateId()}`,
     children: [],
     meta: {
       width: converted.size_x,
@@ -99,13 +101,31 @@ function getChartHolder(item) {
 }
 
 function getChildrenMax(items, attr, layout) {
-  return Math.max.apply(null, items.map(child => (layout[child].meta[attr])));
+  return Math.max.apply(null, items.map(child => layout[child].meta[attr]));
 }
 
 function getChildrenSum(items, attr, layout) {
-  return items.reduce((preValue, child) => (preValue + layout[child].meta[attr]), 0);
+  return items.reduce(
+    (preValue, child) => 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;
 }
@@ -115,7 +135,8 @@ function sortByColId(item1, item2) {
 }
 
 function hasOverlap(positions, xAxis = true) {
-  return positions.slice()
+  return positions
+    .slice()
     .sort(xAxis ? sortByColId : sortByRowId)
     .some((item, index, arr) => {
       if (index === arr.length - 1) {
@@ -123,9 +144,9 @@ function hasOverlap(positions, xAxis = true) {
       }
 
       if (xAxis) {
-        return (item.col + item.size_x) > arr[index + 1].col;
+        return item.col + item.size_x > arr[index + 1].col;
       }
-      return (item.row + item.size_y) > arr[index + 1].row;
+      return item.row + item.size_y > arr[index + 1].row;
     });
 }
 
@@ -158,7 +179,7 @@ function doConvert(positions, level, parent, root) {
     const upper = [];
     const lower = [];
 
-    const isRowDivider = currentItems.every((item) => {
+    const isRowDivider = currentItems.every(item => {
       const { row, size_y } = item;
       if (row + size_y <= currentRow) {
         lower.push(item);
@@ -174,10 +195,10 @@ function doConvert(positions, level, parent, root) {
       currentItems = upper.slice();
       layers.push(lower);
     }
-    currentRow++;
+    currentRow += 1;
   }
 
-  layers.forEach((layer) => {
+  layers.forEach(layer => {
     if (layer.length === 0) {
       return;
     }
@@ -196,7 +217,7 @@ function doConvert(positions, level, parent, root) {
 
     currentItems = layer.slice();
     if (!hasOverlap(currentItems)) {
-      currentItems.sort(sortByColId).forEach((item) => {
+      currentItems.sort(sortByColId).forEach(item => {
         const chartHolder = getChartHolder(item);
         root[chartHolder.id] = chartHolder;
         rowContainer.children.push(chartHolder.id);
@@ -208,7 +229,7 @@ function doConvert(positions, level, parent, root) {
         const upper = [];
         const lower = [];
 
-        const isColDivider = currentItems.every((item) => {
+        const isColDivider = currentItems.every(item => {
           const { col, size_x } = item;
           if (col + size_x <= currentCol) {
             lower.push(item);
@@ -232,7 +253,7 @@ function doConvert(positions, level, parent, root) {
             rowContainer.children.push(colContainer.id);
 
             if (!hasOverlap(lower, false)) {
-              lower.sort(sortByRowId).forEach((item) => {
+              lower.sort(sortByRowId).forEach(item => {
                 const chartHolder = getChartHolder(item);
                 root[chartHolder.id] = chartHolder;
                 colContainer.children.push(chartHolder.id);
@@ -242,37 +263,47 @@ function doConvert(positions, level, parent, root) {
             }
 
             // add col meta
-            colContainer.meta.width = getChildrenMax(colContainer.children, 'width', root);
+            colContainer.meta.width = getChildrenMax(
+              colContainer.children,
+              'width',
+              root,
+            );
           }
 
           currentItems = upper.slice();
         }
-        currentCol++;
+        currentCol += 1;
       }
     }
 
-    rowContainer.meta.width = getChildrenSum(rowContainer.children, 'width', root);
+    rowContainer.meta.width = getChildrenSum(
+      rowContainer.children,
+      'width',
+      root,
+    );
   });
 }
 
-export default function (dashboard) {
+export default function(dashboard) {
   const positions = [];
 
   // position data clean up. some dashboard didn't have position_json
   let { position_json } = dashboard;
   const posDict = {};
   if (Array.isArray(position_json)) {
-    position_json.forEach((position) => {
+    position_json.forEach(position => {
       posDict[position.slice_id] = position;
     });
   } else {
     position_json = [];
   }
 
-  const lastRowId = Math.max(0, Math.max.apply(null,
-    position_json.map(pos => (pos.row + pos.size_y))));
+  const lastRowId = Math.max(
+    0,
+    Math.max.apply(null, position_json.map(pos => pos.row + pos.size_y)),
+  );
   let newSliceCounter = 0;
-  dashboard.slices.forEach((slice) => {
+  dashboard.slices.forEach(slice => {
     const sliceId = slice.slice_id;
     let pos = posDict[sliceId];
     if (!pos) {
@@ -284,7 +315,7 @@ export default function (dashboard) {
         size_y: 16,
         slice_id: String(sliceId),
       };
-      newSliceCounter++;
+      newSliceCounter += 1;
     }
 
     positions.push(pos);
@@ -292,7 +323,6 @@ export default function (dashboard) {
 
   const root = {
     [DASHBOARD_ROOT_ID]: {
-      version: 'v2',
       type: DASHBOARD_ROOT_TYPE,
       id: DASHBOARD_ROOT_ID,
       children: [DASHBOARD_GRID_ID],
@@ -310,13 +340,12 @@ export default function (dashboard) {
   doConvert(positions, 0, root[DASHBOARD_GRID_ID], root);
 
   // remove row's width/height and col's height
-  Object.values(root).forEach((item) => {
+  Object.values(root).forEach(item => {
     if (ROW_TYPE === item.type) {
       const meta = item.meta;
       delete meta.width;
     }
   });
 
-  // console.log(JSON.stringify(root));
   return root;
 }
diff --git a/superset/assets/src/dashboard/v2/util/dnd-reorder.js b/superset/assets/src/dashboard/util/dnd-reorder.js
similarity index 84%
rename from superset/assets/src/dashboard/v2/util/dnd-reorder.js
rename to superset/assets/src/dashboard/util/dnd-reorder.js
index 9a0dedf..76fb56c 100644
--- a/superset/assets/src/dashboard/v2/util/dnd-reorder.js
+++ b/superset/assets/src/dashboard/util/dnd-reorder.js
@@ -6,22 +6,14 @@ export function reorder(list, startIndex, endIndex) {
   return result;
 }
 
-export default function reorderItem({
-  entitiesMap,
-  source,
-  destination,
-}) {
+export default function reorderItem({ entitiesMap, source, destination }) {
   const current = [...entitiesMap[source.id].children];
   const next = [...entitiesMap[destination.id].children];
   const target = current[source.index];
 
   // moving to same list
   if (source.id === destination.id) {
-    const reordered = reorder(
-      current,
-      source.index,
-      destination.index,
-    );
+    const reordered = reorder(current, source.index, destination.index);
 
     const result = {
       ...entitiesMap,
diff --git a/superset/assets/src/dashboard/v2/util/dropOverflowsParent.js b/superset/assets/src/dashboard/util/dropOverflowsParent.js
similarity index 55%
rename from superset/assets/src/dashboard/v2/util/dropOverflowsParent.js
rename to superset/assets/src/dashboard/util/dropOverflowsParent.js
index 0fd0c4e..bc7195f 100644
--- a/superset/assets/src/dashboard/v2/util/dropOverflowsParent.js
+++ b/superset/assets/src/dashboard/util/dropOverflowsParent.js
@@ -1,23 +1,37 @@
 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] || {};
+  const child = isNewComponent
+    ? newComponentFactory(dragging.type)
+    : components[dragging.id] || {};
   const parent = components[destination.id] || {};
   const grandparent = components[grandparentId] || {};
 
-  const grandparentWidth = (grandparent.meta && grandparent.meta.width) || GRID_COLUMN_COUNT;
+  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 });
+  const parentChildWidth =
+    parent.type === COLUMN_TYPE
+      ? (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/src/dashboard/v2/util/findParentId.js b/superset/assets/src/dashboard/util/findParentId.js
similarity index 73%
rename from superset/assets/src/dashboard/v2/util/findParentId.js
rename to superset/assets/src/dashboard/util/findParentId.js
index 0ca15a6..f84b0de 100644
--- a/superset/assets/src/dashboard/v2/util/findParentId.js
+++ b/superset/assets/src/dashboard/util/findParentId.js
@@ -5,7 +5,11 @@ export default function findParentId({ childId, components = {} }) {
   for (let i = 0; i < ids.length - 1; i += 1) {
     const id = ids[i];
     const component = components[id] || {};
-    if (id !== childId && component.children && component.children.includes(childId)) {
+    if (
+      id !== childId &&
+      component.children &&
+      component.children.includes(childId)
+    ) {
       parentId = id;
       break;
     }
diff --git a/superset/assets/src/dashboard/util/getChartIdsFromLayout.js b/superset/assets/src/dashboard/util/getChartIdsFromLayout.js
new file mode 100644
index 0000000..f0963c1
--- /dev/null
+++ b/superset/assets/src/dashboard/util/getChartIdsFromLayout.js
@@ -0,0 +1,8 @@
+export default function getChartIdsFromLayout(layout) {
+  return Object.values(layout).reduce((chartIds, value) => {
+    if (value && value.meta && value.meta.chartId) {
+      chartIds.push(value.meta.chartId);
+    }
+    return chartIds;
+  }, []);
+}
diff --git a/superset/assets/src/dashboard/v2/util/getChildWidth.js b/superset/assets/src/dashboard/util/getChildWidth.js
similarity index 55%
rename from superset/assets/src/dashboard/v2/util/getChildWidth.js
rename to superset/assets/src/dashboard/util/getChildWidth.js
index aa32b96..69d2792 100644
--- a/superset/assets/src/dashboard/v2/util/getChildWidth.js
+++ b/superset/assets/src/dashboard/util/getChildWidth.js
@@ -4,9 +4,9 @@ export default function getTotalChildWidth({ id, components }) {
 
   let width = 0;
 
-  (component.children || []).forEach((childId) => {
-    const child = components[childId];
-    width += child.meta.width || 0;
+  (component.children || []).forEach(childId => {
+    const child = components[childId] || {};
+    width += (child.meta || {}).width || 0;
   });
 
   return width;
diff --git a/superset/assets/src/dashboard/v2/util/getDropPosition.js b/superset/assets/src/dashboard/util/getDropPosition.js
similarity index 80%
rename from superset/assets/src/dashboard/v2/util/getDropPosition.js
rename to superset/assets/src/dashboard/util/getDropPosition.js
index 9605db2..2a02702 100644
--- a/superset/assets/src/dashboard/v2/util/getDropPosition.js
+++ b/superset/assets/src/dashboard/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 {
@@ -22,7 +22,11 @@ export default function getDropPosition(monitor, Component) {
   const draggingItem = monitor.getItem();
 
   // if dropped self on self, do nothing
-  if (!draggingItem || draggingItem.id === component.id || !isDraggingOverShallow) {
+  if (
+    !draggingItem ||
+    draggingItem.id === component.id ||
+    !isDraggingOverShallow
+  ) {
     return null;
   }
 
@@ -34,7 +38,8 @@ export default function getDropPosition(monitor, Component) {
 
   const parentType = parentComponent && parentComponent.type;
   const parentDepth = // see isValidChild.js for why tabs don't increment child depth
-    componentDepth + (parentType === TAB_TYPE || parentType === TABS_TYPE ? 0 : -1);
+    componentDepth +
+    (parentType === TAB_TYPE || parentType === TABS_TYPE ? 0 : -1);
 
   const validSibling = isValidChild({
     parentType,
@@ -47,10 +52,13 @@ export default function getDropPosition(monitor, Component) {
   }
 
   const hasChildren = (component.children || []).length > 0;
-  const childDropOrientation = orientation === 'row' ? 'vertical' : 'horizontal';
-  const siblingDropOrientation = orientation === 'row' ? 'horizontal' : 'vertical';
+  const childDropOrientation =
+    orientation === 'row' ? 'vertical' : 'horizontal';
+  const siblingDropOrientation =
+    orientation === 'row' ? 'horizontal' : 'vertical';
 
-  if (validChild && !validSibling) { // easiest case, insert as child
+  if (validChild && !validSibling) {
+    // easiest case, insert as child
     if (childDropOrientation === 'vertical') {
       return hasChildren ? DROP_RIGHT : DROP_LEFT;
     }
@@ -64,10 +72,12 @@ export default function getDropPosition(monitor, Component) {
   if (validSibling && !validChild) {
     if (siblingDropOrientation === 'vertical') {
       const refMiddleX =
-        refBoundingRect.left + ((refBoundingRect.right - refBoundingRect.left) / 2);
+        refBoundingRect.left +
+        (refBoundingRect.right - refBoundingRect.left) / 2;
       return clientOffset.x < refMiddleX ? DROP_LEFT : DROP_RIGHT;
     }
-    const refMiddleY = refBoundingRect.top + ((refBoundingRect.bottom - refBoundingRect.top) / 2);
+    const refMiddleY =
+      refBoundingRect.top + (refBoundingRect.bottom - refBoundingRect.top) / 2;
     return clientOffset.y < refMiddleY ? DROP_TOP : DROP_BOTTOM;
   }
 
diff --git a/superset/assets/src/dashboard/v2/util/headerStyleOptions.js b/superset/assets/src/dashboard/util/headerStyleOptions.js
similarity index 89%
rename from superset/assets/src/dashboard/v2/util/headerStyleOptions.js
rename to superset/assets/src/dashboard/util/headerStyleOptions.js
index 309d482..7efa040 100644
--- a/superset/assets/src/dashboard/v2/util/headerStyleOptions.js
+++ b/superset/assets/src/dashboard/util/headerStyleOptions.js
@@ -1,4 +1,4 @@
-import { t } from '../../../locales';
+import { t } from '../../locales';
 import { SMALL_HEADER, MEDIUM_HEADER, LARGE_HEADER } from './constants';
 
 export default [
diff --git a/superset/assets/src/dashboard/v2/util/isValidChild.js b/superset/assets/src/dashboard/util/isValidChild.js
similarity index 65%
rename from superset/assets/src/dashboard/v2/util/isValidChild.js
rename to superset/assets/src/dashboard/util/isValidChild.js
index 66942f0..d789f45 100644
--- a/superset/assets/src/dashboard/v2/util/isValidChild.js
+++ b/superset/assets/src/dashboard/util/isValidChild.js
@@ -1,19 +1,19 @@
 /* eslint max-len: 0 */
 /**
-  * When determining if a component is a valid child of another component we must consider both
-  *   - parent + child component types
-  *   - component depth, or depth of nesting of container components
-  *
-  * We consider types because some components aren't containers (e.g. a heading) and we consider
-  * depth to prevent infinite nesting of container components.
-  *
-  * The following example container nestings should be valid, which means that some containers
-  * don't increase the (depth) of their children, namely tabs and tab:
-  *   (a) root (0) > grid (1) >                         row (2) > column (3) > row (4) > non-container (5)
-  *   (b) root (0) > grid (1) >    tabs (2) > tab (2) > row (2) > column (3) > row (4) > non-container (5)
-  *   (c) root (0) > top-tab (1) >                      row (2) > column (3) > row (4) > non-container (5)
-  *   (d) root (0) > top-tab (1) > tabs (2) > tab (2) > row (2) > column (3) > row (4) > non-container (5)
-  */
+ * When determining if a component is a valid child of another component we must consider both
+ *   - parent + child component types
+ *   - component depth, or depth of nesting of container components
+ *
+ * We consider types because some components aren't containers (e.g. a heading) and we consider
+ * depth to prevent infinite nesting of container components.
+ *
+ * The following example container nestings should be valid, which means that some containers
+ * don't increase the (depth) of their children, namely tabs and tab:
+ *   (a) root (0) > grid (1) >                         row (2) > column (3) > row (4) > non-container (5)
+ *   (b) root (0) > grid (1) >    tabs (2) > tab (2) > row (2) > column (3) > row (4) > non-container (5)
+ *   (c) root (0) > top-tab (1) >                      row (2) > column (3) > row (4) > non-container (5)
+ *   (d) root (0) > top-tab (1) > tabs (2) > tab (2) > row (2) > column (3) > row (4) > non-container (5)
+ */
 import {
   CHART_TYPE,
   COLUMN_TYPE,
diff --git a/superset/assets/src/dashboard/v2/util/newComponentFactory.js b/superset/assets/src/dashboard/util/newComponentFactory.js
similarity index 81%
rename from superset/assets/src/dashboard/v2/util/newComponentFactory.js
rename to superset/assets/src/dashboard/util/newComponentFactory.js
index b428ddd..4e2de37 100644
--- a/superset/assets/src/dashboard/v2/util/newComponentFactory.js
+++ b/superset/assets/src/dashboard/util/newComponentFactory.js
@@ -1,3 +1,5 @@
+import shortid from 'shortid';
+
 import {
   CHART_TYPE,
   COLUMN_TYPE,
@@ -9,10 +11,7 @@ import {
   TAB_TYPE,
 } from './componentTypes';
 
-import {
-  MEDIUM_HEADER,
-  BACKGROUND_TRANSPARENT,
-} from './constants';
+import { MEDIUM_HEADER, BACKGROUND_TRANSPARENT } from './constants';
 
 const typeToDefaultMetaData = {
   [CHART_TYPE]: { width: 3, height: 30 },
@@ -29,9 +28,8 @@ const typeToDefaultMetaData = {
   [TAB_TYPE]: { text: 'New Tab' },
 };
 
-// @TODO this should be replaced by a more robust algorithm
 function uuid(type) {
-  return `${type}-${Math.random().toString(16)}`;
+  return `${type}-${shortid.generate()}`;
 }
 
 export default function entityFactory(type, meta) {
diff --git a/superset/assets/src/dashboard/v2/util/newEntitiesFromDrop.js b/superset/assets/src/dashboard/util/newEntitiesFromDrop.js
similarity index 81%
rename from superset/assets/src/dashboard/v2/util/newEntitiesFromDrop.js
rename to superset/assets/src/dashboard/util/newEntitiesFromDrop.js
index 7cccc5f..7fe7f4e 100644
--- a/superset/assets/src/dashboard/v2/util/newEntitiesFromDrop.js
+++ b/superset/assets/src/dashboard/util/newEntitiesFromDrop.js
@@ -1,11 +1,7 @@
 import shouldWrapChildInRow from './shouldWrapChildInRow';
 import newComponentFactory from './newComponentFactory';
 
-import {
-  ROW_TYPE,
-  TABS_TYPE,
-  TAB_TYPE,
-} from './componentTypes';
+import { ROW_TYPE, TABS_TYPE, TAB_TYPE } from './componentTypes';
 
 export default function newEntitiesFromDrop({ dropResult, components }) {
   const { dragging, destination } = dropResult;
@@ -15,7 +11,10 @@ export default function newEntitiesFromDrop({ dropResult, components }) {
   const dropEntity = components[destination.id];
   const dropType = dropEntity.type;
   let newDropChild = newComponentFactory(dragType, dragMeta);
-  const wrapChildInRow = shouldWrapChildInRow({ parentType: dropType, childType: dragType });
+  const wrapChildInRow = shouldWrapChildInRow({
+    parentType: dropType,
+    childType: dragType,
+  });
 
   const newEntities = {
     [newDropChild.id]: newDropChild,
@@ -26,7 +25,8 @@ export default function newEntitiesFromDrop({ dropResult, components }) {
     rowWrapper.children = [newDropChild.id];
     newEntities[rowWrapper.id] = rowWrapper;
     newDropChild = rowWrapper;
-  } else if (dragType === TABS_TYPE) { // create a new tab component
+  } else if (dragType === TABS_TYPE) {
+    // create a new tab component
     const tabChild = newComponentFactory(TAB_TYPE);
     newDropChild.children = [tabChild.id];
     newEntities[tabChild.id] = tabChild;
diff --git a/superset/assets/src/dashboard/v2/util/propShapes.jsx b/superset/assets/src/dashboard/util/propShapes.jsx
similarity index 88%
rename from superset/assets/src/dashboard/v2/util/propShapes.jsx
rename to superset/assets/src/dashboard/util/propShapes.jsx
index 388c726..73a10b0 100644
--- a/superset/assets/src/dashboard/v2/util/propShapes.jsx
+++ b/superset/assets/src/dashboard/util/propShapes.jsx
@@ -2,13 +2,16 @@ import PropTypes from 'prop-types';
 import componentTypes from './componentTypes';
 import backgroundStyleOptions from './backgroundStyleOptions';
 import headerStyleOptions from './headerStyleOptions';
-import { INFO_TOAST, SUCCESS_TOAST, WARNING_TOAST, DANGER_TOAST } from './constants';
+import {
+  INFO_TOAST,
+  SUCCESS_TOAST,
+  WARNING_TOAST,
+  DANGER_TOAST,
+} from './constants';
 
-export const componentShape = PropTypes.shape({ // eslint-disable-line
+export const componentShape = PropTypes.shape({
   id: PropTypes.string.isRequired,
-  type: PropTypes.oneOf(
-    Object.values(componentTypes),
-  ).isRequired,
+  type: PropTypes.oneOf(Object.values(componentTypes)).isRequired,
   children: PropTypes.arrayOf(PropTypes.string),
   meta: PropTypes.shape({
     // Dimensions
@@ -25,7 +28,12 @@ export const componentShape = PropTypes.shape({ // eslint-disable-line
 
 export const toastShape = PropTypes.shape({
   id: PropTypes.string.isRequired,
-  toastType: PropTypes.oneOf([INFO_TOAST, SUCCESS_TOAST, WARNING_TOAST, DANGER_TOAST]).isRequired,
+  toastType: PropTypes.oneOf([
+    INFO_TOAST,
+    SUCCESS_TOAST,
+    WARNING_TOAST,
+    DANGER_TOAST,
+  ]).isRequired,
   text: PropTypes.string.isRequired,
 });
 
diff --git a/superset/assets/src/dashboard/v2/util/resizableConfig.js b/superset/assets/src/dashboard/util/resizableConfig.js
similarity index 100%
rename from superset/assets/src/dashboard/v2/util/resizableConfig.js
rename to superset/assets/src/dashboard/util/resizableConfig.js
diff --git a/superset/assets/src/dashboard/v2/util/shouldWrapChildInRow.js b/superset/assets/src/dashboard/util/shouldWrapChildInRow.js
similarity index 100%
rename from superset/assets/src/dashboard/v2/util/shouldWrapChildInRow.js
rename to superset/assets/src/dashboard/util/shouldWrapChildInRow.js
diff --git a/superset/assets/src/dashboard/v2/actions/editMode.js b/superset/assets/src/dashboard/v2/actions/editMode.js
deleted file mode 100644
index 0a849ea..0000000
--- a/superset/assets/src/dashboard/v2/actions/editMode.js
+++ /dev/null
@@ -1,9 +0,0 @@
-export const SET_EDIT_MODE = 'SET_EDIT_MODE';
-export function setEditMode(editMode) {
-  return {
-    type: SET_EDIT_MODE,
-    payload: {
-      editMode,
-    },
-  };
-}
diff --git a/superset/assets/src/dashboard/v2/components/Dashboard.jsx b/superset/assets/src/dashboard/v2/components/Dashboard.jsx
deleted file mode 100644
index ffd1280..0000000
--- a/superset/assets/src/dashboard/v2/components/Dashboard.jsx
+++ /dev/null
@@ -1,30 +0,0 @@
-import React from 'react';
-import PropTypes from 'prop-types';
-
-import DashboardBuilder from '../containers/DashboardBuilder';
-
-import '../stylesheets/index.less';
-
-const propTypes = {
-  actions: PropTypes.shape({
-    updateDashboardTitle: PropTypes.func.isRequired,
-    setEditMode: PropTypes.func.isRequired,
-  }),
-  editMode: PropTypes.bool,
-};
-
-const defaultProps = {
-  editMode: true,
-};
-
-class Dashboard extends React.Component {
-  render() {
-    // @TODO delete this component?
-    return <DashboardBuilder />;
-  }
-}
-
-Dashboard.propTypes = propTypes;
-Dashboard.defaultProps = defaultProps;
-
-export default Dashboard;
diff --git a/superset/assets/src/dashboard/v2/components/DashboardHeader.jsx b/superset/assets/src/dashboard/v2/components/DashboardHeader.jsx
deleted file mode 100644
index d3ec7ac..0000000
--- a/superset/assets/src/dashboard/v2/components/DashboardHeader.jsx
+++ /dev/null
@@ -1,99 +0,0 @@
-import React from 'react';
-import PropTypes from 'prop-types';
-import { ButtonGroup, ButtonToolbar, DropdownButton, MenuItem } from 'react-bootstrap';
-
-import Button from '../../../components/Button';
-import { componentShape } from '../util/propShapes';
-import EditableTitle from '../../../components/EditableTitle';
-
-const propTypes = {
-  editMode: PropTypes.bool.isRequired,
-  component: componentShape.isRequired,
-
-  // redux
-  updateComponents: PropTypes.func.isRequired,
-  onUndo: PropTypes.func.isRequired,
-  onRedo: PropTypes.func.isRequired,
-  canUndo: PropTypes.bool.isRequired,
-  canRedo: PropTypes.bool.isRequired,
-  setEditMode: PropTypes.func.isRequired,
-};
-
-class DashboardHeader extends React.Component {
-  constructor(props) {
-    super(props);
-    this.handleChangeText = this.handleChangeText.bind(this);
-    this.toggleEditMode = this.toggleEditMode.bind(this);
-  }
-
-  toggleEditMode() {
-    this.props.setEditMode(!this.props.editMode);
-  }
-
-  handleChangeText(nextText) {
-    const { updateComponents, component } = this.props;
-    if (nextText && component.meta.text !== nextText) {
-      updateComponents({
-        [component.id]: {
-          ...component,
-          meta: {
-            ...component.meta,
-            text: nextText,
-          },
-        },
-      });
-    }
-  }
-
-  render() {
-    const { component, onUndo, onRedo, canUndo, canRedo, editMode } = this.props;
-
-    return (
-      <div className="dashboard-header">
-        <div className="dashboard-component-header header-large">
-          <EditableTitle
-            title={'Test title'}
-            onSaveTitle={this.handleChangeText}
-            showTooltip={false}
-            canEdit={editMode}
-          />
-        </div>
-        <ButtonToolbar>
-          <ButtonGroup>
-            <Button
-              bsSize="small"
-              onClick={onUndo}
-              disabled={!canUndo}
-            >
-              Undo
-            </Button>
-            <Button
-              bsSize="small"
-              onClick={onRedo}
-              disabled={!canRedo}
-            >
-              Redo
-            </Button>
-          </ButtonGroup>
-
-          <DropdownButton title="Actions" bsSize="small" id="btn-dashboard-actions">
-            <MenuItem>Action 1</MenuItem>
-            <MenuItem>Action 2</MenuItem>
-            <MenuItem>Action 3</MenuItem>
-          </DropdownButton>
-
-          <Button
-            bsStyle="primary"
-            onClick={this.toggleEditMode}
-          >
-            {editMode ? 'Save changes' : 'Edit dashboard'}
-          </Button>
-        </ButtonToolbar>
-      </div>
-    );
-  }
-}
-
-DashboardHeader.propTypes = propTypes;
-
-export default DashboardHeader;
diff --git a/superset/assets/src/dashboard/v2/components/StaticDashboard.jsx b/superset/assets/src/dashboard/v2/components/StaticDashboard.jsx
deleted file mode 100644
index 4fd2397..0000000
--- a/superset/assets/src/dashboard/v2/components/StaticDashboard.jsx
+++ /dev/null
@@ -1,19 +0,0 @@
-import React from 'react';
-// import PropTypes from 'prop-types';
-
-const propTypes = {
-};
-
-class StaticDashboard extends React.Component {
-  render() {
-    return (
-      <div>
-        Static dashboard ...
-      </div>
-    );
-  }
-}
-
-StaticDashboard.propTypes = propTypes;
-
-export default StaticDashboard;
diff --git a/superset/assets/src/dashboard/v2/components/gridComponents/new/NewChart.jsx b/superset/assets/src/dashboard/v2/components/gridComponents/new/NewChart.jsx
deleted file mode 100644
index 0255755..0000000
--- a/superset/assets/src/dashboard/v2/components/gridComponents/new/NewChart.jsx
+++ /dev/null
@@ -1,24 +0,0 @@
-import React from 'react';
-// import PropTypes from 'prop-types';
-
-import { CHART_TYPE } from '../../../util/componentTypes';
-import { NEW_CHART_ID } from '../../../util/constants';
-import DraggableNewComponent from './DraggableNewComponent';
-
-const propTypes = {
-};
-
-export default class DraggableNewChart extends React.PureComponent {
-  render() {
-    return (
-      <DraggableNewComponent
-        id={NEW_CHART_ID}
-        type={CHART_TYPE}
-        label="Chart"
-        className="fa fa-area-chart"
-      />
-    );
-  }
-}
-
-DraggableNewChart.propTypes = propTypes;
diff --git a/superset/assets/src/dashboard/v2/components/gridComponents/new/NewColumn.jsx b/superset/assets/src/dashboard/v2/components/gridComponents/new/NewColumn.jsx
deleted file mode 100644
index 654c60b..0000000
--- a/superset/assets/src/dashboard/v2/components/gridComponents/new/NewColumn.jsx
+++ /dev/null
@@ -1,24 +0,0 @@
-import React from 'react';
-// import PropTypes from 'prop-types';
-
-import { COLUMN_TYPE } from '../../../util/componentTypes';
-import { NEW_COLUMN_ID } from '../../../util/constants';
-import DraggableNewComponent from './DraggableNewComponent';
-
-const propTypes = {
-};
-
-export default class DraggableNewColumn extends React.PureComponent {
-  render() {
-    return (
-      <DraggableNewComponent
-        id={NEW_COLUMN_ID}
-        type={COLUMN_TYPE}
-        label="Column"
-        className="fa fa-long-arrow-down"
-      />
-    );
-  }
-}
-
-DraggableNewColumn.propTypes = propTypes;
diff --git a/superset/assets/src/dashboard/v2/components/gridComponents/new/NewDivider.jsx b/superset/assets/src/dashboard/v2/components/gridComponents/new/NewDivider.jsx
deleted file mode 100644
index 5d70041..0000000
--- a/superset/assets/src/dashboard/v2/components/gridComponents/new/NewDivider.jsx
+++ /dev/null
@@ -1,24 +0,0 @@
-import React from 'react';
-// import PropTypes from 'prop-types';
-
-import { DIVIDER_TYPE } from '../../../util/componentTypes';
-import { NEW_DIVIDER_ID } from '../../../util/constants';
-import DraggableNewComponent from './DraggableNewComponent';
-
-const propTypes = {
-};
-
-export default class DraggableNewDivider extends React.PureComponent {
-  render() {
-    return (
-      <DraggableNewComponent
-        id={NEW_DIVIDER_ID}
-        type={DIVIDER_TYPE}
-        label="Divider"
-        className="divider-placeholder"
-      />
-    );
-  }
-}
-
-DraggableNewDivider.propTypes = propTypes;
diff --git a/superset/assets/src/dashboard/v2/components/gridComponents/new/NewHeader.jsx b/superset/assets/src/dashboard/v2/components/gridComponents/new/NewHeader.jsx
deleted file mode 100644
index d207a9c..0000000
--- a/superset/assets/src/dashboard/v2/components/gridComponents/new/NewHeader.jsx
+++ /dev/null
@@ -1,24 +0,0 @@
-import React from 'react';
-// import PropTypes from 'prop-types';
-
-import { HEADER_TYPE } from '../../../util/componentTypes';
-import { NEW_HEADER_ID } from '../../../util/constants';
-import DraggableNewComponent from './DraggableNewComponent';
-
-const propTypes = {
-};
-
-export default class DraggableNewHeader extends React.Component {
-  render() {
-    return (
-      <DraggableNewComponent
-        id={NEW_HEADER_ID}
-        type={HEADER_TYPE}
-        label="Header"
-        className="fa fa-header"
-      />
-    );
-  }
-}
-
-DraggableNewHeader.propTypes = propTypes;
diff --git a/superset/assets/src/dashboard/v2/components/gridComponents/new/NewRow.jsx b/superset/assets/src/dashboard/v2/components/gridComponents/new/NewRow.jsx
deleted file mode 100644
index 1d9ab10..0000000
--- a/superset/assets/src/dashboard/v2/components/gridComponents/new/NewRow.jsx
+++ /dev/null
@@ -1,23 +0,0 @@
-import React from 'react';
-
-import { ROW_TYPE } from '../../../util/componentTypes';
-import { NEW_ROW_ID } from '../../../util/constants';
-import DraggableNewComponent from './DraggableNewComponent';
-
-const propTypes = {
-};
-
-export default class DraggableNewRow extends React.PureComponent {
-  render() {
-    return (
-      <DraggableNewComponent
-        id={NEW_ROW_ID}
-        type={ROW_TYPE}
-        label="Row"
-        className="fa fa-long-arrow-right"
-      />
-    );
-  }
-}
-
-DraggableNewRow.propTypes = propTypes;
diff --git a/superset/assets/src/dashboard/v2/components/gridComponents/new/NewTabs.jsx b/superset/assets/src/dashboard/v2/components/gridComponents/new/NewTabs.jsx
deleted file mode 100644
index a473281..0000000
--- a/superset/assets/src/dashboard/v2/components/gridComponents/new/NewTabs.jsx
+++ /dev/null
@@ -1,24 +0,0 @@
-import React from 'react';
-// import PropTypes from 'prop-types';
-
-import { TABS_TYPE } from '../../../util/componentTypes';
-import { NEW_TABS_ID } from '../../../util/constants';
-import DraggableNewComponent from './DraggableNewComponent';
-
-const propTypes = {
-};
-
-export default class DraggableNewTabs extends React.PureComponent {
-  render() {
-    return (
-      <DraggableNewComponent
-        id={NEW_TABS_ID}
-        type={TABS_TYPE}
-        label="Tabs"
-        className="fa fa-window-restore"
-      />
-    );
-  }
-}
-
-DraggableNewTabs.propTypes = propTypes;
diff --git a/superset/assets/src/dashboard/v2/reducers/editMode.js b/superset/assets/src/dashboard/v2/reducers/editMode.js
deleted file mode 100644
index b1a1630..0000000
--- a/superset/assets/src/dashboard/v2/reducers/editMode.js
+++ /dev/null
@@ -1,11 +0,0 @@
-import { SET_EDIT_MODE } from '../actions/editMode';
-
-export default function editModeReducer(editMode = false, action) {
-  switch (action.type) {
-    case SET_EDIT_MODE:
-      return action.payload.editMode;
-
-    default:
-      return editMode;
-  }
-}
diff --git a/superset/assets/src/dashboard/v2/reducers/index.js b/superset/assets/src/dashboard/v2/reducers/index.js
deleted file mode 100644
index 061255d..0000000
--- a/superset/assets/src/dashboard/v2/reducers/index.js
+++ /dev/null
@@ -1,8 +0,0 @@
-import undoable, { distinctState } from 'redux-undo';
-
-import dashboardLayout from './dashboardLayout';
-
-export default undoable(dashboardLayout, {
-  limit: 15,
-  filter: distinctState(),
-});
diff --git a/superset/assets/src/dashboard/v2/stylesheets/components/chart.less b/superset/assets/src/dashboard/v2/stylesheets/components/chart.less
deleted file mode 100644
index ce03797..0000000
--- a/superset/assets/src/dashboard/v2/stylesheets/components/chart.less
+++ /dev/null
@@ -1,20 +0,0 @@
-.dashboard-component-chart {
-  width: 100%;
-  height: 100%;
-  color: @gray-dark;
-  background-color: white;
-  padding: 16px;
-  display: flex;
-  align-items: center;
-  justify-content: center;
-  position: relative;
-}
-
-.dashboard-component-chart .fa {
-  //font-size: 100px;
-  opacity: 0.3;
-}
-
-.dashboard-v2--editing .dashboard-component-chart:hover {
-  box-shadow: inset 0 0 0 1px @gray-light;
-}
diff --git a/superset/assets/src/dashboard/v2/util/backgroundStyleOptions.js b/superset/assets/src/dashboard/v2/util/backgroundStyleOptions.js
deleted file mode 100644
index cda678f..0000000
--- a/superset/assets/src/dashboard/v2/util/backgroundStyleOptions.js
+++ /dev/null
@@ -1,7 +0,0 @@
-import { t } from '../../../locales';
-import { BACKGROUND_TRANSPARENT, BACKGROUND_WHITE } from './constants';
-
-export default [
-  { value: BACKGROUND_TRANSPARENT, label: t('Transparent'), className: 'background--transparent' },
-  { value: BACKGROUND_WHITE, label: t('White'), className: 'background--white' },
-];
diff --git a/superset/assets/src/dashboard/v2/util/componentIsResizable.js b/superset/assets/src/dashboard/v2/util/componentIsResizable.js
deleted file mode 100644
index c0016f3..0000000
--- a/superset/assets/src/dashboard/v2/util/componentIsResizable.js
+++ /dev/null
@@ -1,13 +0,0 @@
-import {
-  COLUMN_TYPE,
-  CHART_TYPE,
-  MARKDOWN_TYPE,
-} from './componentTypes';
-
-export default function componentIsResizable(entity) {
-  return [
-    COLUMN_TYPE,
-    CHART_TYPE,
-    MARKDOWN_TYPE,
-  ].indexOf(entity.type) > -1;
-}
diff --git a/superset/assets/src/explore/components/ExploreChartHeader.jsx b/superset/assets/src/explore/components/ExploreChartHeader.jsx
index 19416b0..3825335 100644
--- a/superset/assets/src/explore/components/ExploreChartHeader.jsx
+++ b/superset/assets/src/explore/components/ExploreChartHeader.jsx
@@ -1,7 +1,7 @@
 import React from 'react';
 import PropTypes from 'prop-types';
 
-import { chartPropShape } from '../../dashboard/v2/util/propShapes';
+import { chartPropShape } from '../../dashboard/util/propShapes';
 import ExploreActionButtons from './ExploreActionButtons';
 import RowCountLabel from './RowCountLabel';
 import EditableTitle from '../../components/EditableTitle';
diff --git a/superset/assets/src/explore/components/ExploreChartPanel.jsx b/superset/assets/src/explore/components/ExploreChartPanel.jsx
index 21c6a64..bcda75d 100644
--- a/superset/assets/src/explore/components/ExploreChartPanel.jsx
+++ b/superset/assets/src/explore/components/ExploreChartPanel.jsx
@@ -3,7 +3,7 @@ import React from 'react';
 import PropTypes from 'prop-types';
 import { Panel } from 'react-bootstrap';
 
-import { chartPropShape } from '../../dashboard/v2/util/propShapes';
+import { chartPropShape } from '../../dashboard/util/propShapes';
 import ChartContainer from '../../chart/ChartContainer';
 import ExploreChartHeader from './ExploreChartHeader';
 
@@ -38,14 +38,15 @@ class ExploreChartPanel extends React.PureComponent {
   }
 
   renderChart() {
+    const { chart } = this.props;
     return (
       <ChartContainer
+        chartId={chart.id}
         containerId={this.props.containerId}
         datasource={this.props.datasource}
         formData={this.props.form_data}
         height={this.getHeight()}
         slice={this.props.slice}
-        chartId={this.props.chart.id}
         setControlValue={this.props.actions.setControlValue}
         timeout={this.props.timeout}
         vizType={this.props.vizType}
@@ -53,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/src/explore/components/ExploreViewContainer.jsx b/superset/assets/src/explore/components/ExploreViewContainer.jsx
index d4e718b..9155c22 100644
--- a/superset/assets/src/explore/components/ExploreViewContainer.jsx
+++ b/superset/assets/src/explore/components/ExploreViewContainer.jsx
@@ -11,7 +11,7 @@ import QueryAndSaveBtns from './QueryAndSaveBtns';
 import { getExploreUrlAndPayload, getExploreLongUrl } from '../exploreUtils';
 import { areObjectsEqual } from '../../reduxUtils';
 import { getFormDataFromControls } from '../stores/store';
-import { chartPropShape } from '../../dashboard/v2/util/propShapes';
+import { chartPropShape } from '../../dashboard/util/propShapes';
 import * as exploreActions from '../actions/exploreActions';
 import * as saveModalActions from '../actions/saveModalActions';
 import * as chartActions from '../../chart/chartAction';
diff --git a/superset/assets/src/visualizations/nvd3_vis.css b/superset/assets/src/visualizations/nvd3_vis.css
index fed0d01..f7539e1 100644
--- a/superset/assets/src/visualizations/nvd3_vis.css
+++ b/superset/assets/src/visualizations/nvd3_vis.css
@@ -63,4 +63,3 @@ g.opacityMedium path, line.opacityMedium {
 g.opacityHigh path, line.opacityHigh {
   stroke-opacity: .8
 }
-
diff --git a/superset/assets/stylesheets/dashboard.less b/superset/assets/stylesheets/dashboard.less
deleted file mode 100644
index b812a42..0000000
--- a/superset/assets/stylesheets/dashboard.less
+++ /dev/null
@@ -1,156 +0,0 @@
-@import "./less/cosmo/variables.less";
-
-.dashboard a i {
-  cursor: pointer;
-}
-.dashboard i.drag {
-  cursor: move !important;
-}
-.dashboard .slice-grid .preview-holder {
-  z-index: 1;
-  position: absolute;
-  background-color: #AAA;
-  border-color: #AAA;
-  opacity: 0.3;
-}
-.dashboard .widget {
-  position: absolute;
-  top: 16px;
-  left: 16px;
-  box-shadow: none;
-  background-color: transparent;
-  overflow: visible;
-}
-.dashboard .chart-header {
-  .dropdown.btn-group {
-    position: absolute;
-    top: 0;
-    right: 0;
-  }
-
-  .dropdown-menu.dropdown-menu-right {
-    right: 7px;
-    top: -3px
-  }
-}
-
-.slice-header-controls-trigger {
-  border: 0;
-  padding: 0 0 0 20px;
-  background: none;
-  outline: none;
-  box-shadow: none;
-  color: #263238;
-
-  &.is-cached {
-    color: red;
-  }
-
-  &:hover, &:focus {
-    background: none;
-    cursor: pointer;
-  }
-
-  .controls-container.dropdown-menu {
-    top: 0;
-    left: unset;
-    right: 10px;
-
-    &.is-open {
-      display: block;
-    }
-
-    & li {
-      white-space: nowrap;
-    }
-  }
-}
-.slice-grid .slice_container {
-  background-color: #fff;
-}
-
-.dashboard .slice-grid .dragging,
-.dashboard .slice-grid .resizing {
-  opacity: 0.5;
-}
-.dashboard img.loading {
-  width: 20px;
-  margin: 5px;
-  position: absolute;
-}
-
-.dashboard .slice_title {
-  text-align: center;
-  font-weight: bold;
-  font-size: 14px;
-  padding: 5px;
-}
-.dashboard div.slice_content {
-  width: 100%;
-  height: 100%;
-}
-
-.modal img.loading {
-  width: 50px;
-  margin: 0;
-  position: relative;
-}
-
-.react-bs-container-body {
-  max-height: 400px;
-  overflow-y: auto;
-}
-
-.hidden, #pageDropDown {
-  display: none;
-}
-
-.slice-cell {
-  box-shadow: 0px 0px 20px 5px rgba(0,0,0,0);
-  transition: box-shadow 1s ease-in;
-
-  .dropdown,
-  .dropdown-menu {
-    .fa {
-      font-size: 14px;
-    }
-  }
-}
-
-.slice-cell-highlight {
-  box-shadow: 0px 0px 20px 5px rgba(0,0,0,0.2);
-  height: 100%;
-}
-
-.slice-cell .editable-title input[type="button"] {
-  font-weight: bold;
-}
-
-.chart-container {
-  box-sizing: border-box;
-}
-
-.chart-header .header {
-  font-size: 16px;
-  margin: 0 -10px;
-}
-.ace_gutter {
-    z-index: 0;
-}
-.ace_content {
-    z-index: 0;
-}
-.ace_scrollbar {
-    z-index: 0;
-}
-.slice_container .alert {
-    margin: 10px;
-}
-
-i.danger {
-  color: red;
-}
-
-i.warning {
-  color: orange;
-}
diff --git a/superset/assets/stylesheets/superset.less b/superset/assets/stylesheets/superset.less
index 6987544..d756551 100644
--- a/superset/assets/stylesheets/superset.less
+++ b/superset/assets/stylesheets/superset.less
@@ -141,7 +141,7 @@ div.navbar {
 img.loading {
   width: 40px;
   position: relative;
-  z-index: 10;
+  z-index: 1;
   margin: 10px;
 }
 img.viz-thumb-option {
diff --git a/superset/assets/stylesheets/welcome.css b/superset/assets/stylesheets/welcome.css
index 8e2496e..1f72852 100644
--- a/superset/assets/stylesheets/welcome.css
+++ b/superset/assets/stylesheets/welcome.css
@@ -3,7 +3,7 @@
 }
 
 img.loading {
-    width: 25px;
+  width: 25px;
 }
 
 .welcome table {
diff --git a/superset/assets/yarn.lock b/superset/assets/yarn.lock
index 5ebc447..77ca84d 100644
--- a/superset/assets/yarn.lock
+++ b/superset/assets/yarn.lock
@@ -332,6 +332,13 @@
   dependencies:
     lodash "^4.0.8"
 
+"@vx/responsive@0.0.153":
+  version "0.0.153"
+  resolved "https://registry.yarnpkg.com/@vx/responsive/-/responsive-0.0.153.tgz#2ce7e819341d2e59ff4151b40e5792aea460e202"
+  dependencies:
+    lodash "^4.0.8"
+    resize-observer-polyfill "1.5.0"
+
 "@vx/scale@0.0.121":
   version "0.0.121"
   resolved "https://registry.yarnpkg.com/@vx/scale/-/scale-0.0.121.tgz#5f49ea2060469ded0bf0e3ef5a5bb1416b81180e"
@@ -418,6 +425,13 @@
     classnames "^2.2.5"
     prop-types "^15.5.10"
 
+JSONStream@^1.3.2:
+  version "1.3.2"
+  resolved "https://registry.yarnpkg.com/JSONStream/-/JSONStream-1.3.2.tgz#c102371b6ec3a7cf3b847ca00c20bb0fce4c6dea"
+  dependencies:
+    jsonparse "^1.2.0"
+    through ">=2.2.7 <3"
+
 "JSV@>= 4.0.x":
   version "4.0.2"
   resolved "https://registry.yarnpkg.com/JSV/-/JSV-4.0.2.tgz#d077f6825571f82132f9dffaed587b4029feff57"
@@ -426,7 +440,7 @@ abab@^1.0.3:
   version "1.0.4"
   resolved "https://registry.yarnpkg.com/abab/-/abab-1.0.4.tgz#5faad9c2c07f60dd76770f71cf025b62a63cfd4e"
 
-abbrev@1, abbrev@^1.0.7:
+abbrev@1, abbrev@^1.0.7, abbrev@~1.1.1:
   version "1.1.1"
   resolved "https://registry.yarnpkg.com/abbrev/-/abbrev-1.1.1.tgz#f8f2c887ad10bf67f634f005b6987fed3179aac8"
 
@@ -474,6 +488,18 @@ acorn@^5.5.0:
   version "5.5.3"
   resolved "https://registry.yarnpkg.com/acorn/-/acorn-5.5.3.tgz#f473dd47e0277a08e28e9bec5aeeb04751f0b8c9"
 
+agent-base@4, agent-base@^4.1.0:
+  version "4.2.0"
+  resolved "https://registry.yarnpkg.com/agent-base/-/agent-base-4.2.0.tgz#9838b5c3392b962bad031e6a4c5e1024abec45ce"
+  dependencies:
+    es6-promisify "^5.0.0"
+
+agentkeepalive@^3.3.0:
+  version "3.4.1"
+  resolved "https://registry.yarnpkg.com/agentkeepalive/-/agentkeepalive-3.4.1.tgz#aa95aebc3a749bca5ed53e3880a09f5235b48f0c"
+  dependencies:
+    humanize-ms "^1.2.1"
+
 ajv-keywords@^2.0.0:
   version "2.1.0"
   resolved "https://registry.yarnpkg.com/ajv-keywords/-/ajv-keywords-2.1.0.tgz#a296e17f7bfae7c1ce4f7e0de53d29cb32162df0"
@@ -541,7 +567,7 @@ ansi-regex@^2.0.0:
   version "2.1.1"
   resolved "https://registry.yarnpkg.com/ansi-regex/-/ansi-regex-2.1.1.tgz#c3b33ab5ee360d86e0e628f0468ae7ef27d654df"
 
-ansi-regex@^3.0.0:
+ansi-regex@^3.0.0, ansi-regex@~3.0.0:
   version "3.0.0"
   resolved "https://registry.yarnpkg.com/ansi-regex/-/ansi-regex-3.0.0.tgz#ed0317c322064f79466c02966bddb605ab37d998"
 
@@ -609,7 +635,7 @@ application-config@~0.1.1:
     application-config-path "^0.1.0"
     mkdirp "^0.5.1"
 
-aproba@^1.0.3, aproba@^1.1.1:
+aproba@^1.0.3, aproba@^1.1.1, aproba@^1.1.2, aproba@~1.2.0:
   version "1.2.0"
   resolved "https://registry.yarnpkg.com/aproba/-/aproba-1.2.0.tgz#6802e6264efd18c790a1b0d517f0f2627bf2c94a"
 
@@ -690,7 +716,7 @@ arrify@^1.0.0:
   version "1.0.1"
   resolved "https://registry.yarnpkg.com/arrify/-/arrify-1.0.1.tgz#898508da2226f380df904728456849c1501a4b0d"
 
-asap@^2.0.0, asap@^2.0.3, asap@~2.0.3, asap@~2.0.5:
+asap@^2.0.0, asap@^2.0.3, asap@^2.0.6, asap@~2.0.3, asap@~2.0.5:
   version "2.0.6"
   resolved "https://registry.yarnpkg.com/asap/-/asap-2.0.6.tgz#e50347611d7e690943208bbdafebcbc2fb866d46"
 
@@ -1457,6 +1483,16 @@ big.js@^3.1.3:
   version "3.2.0"
   resolved "https://registry.yarnpkg.com/big.js/-/big.js-3.2.0.tgz#a5fc298b81b9e0dca2e458824784b65c52ba588e"
 
+bin-links@^1.1.0:
+  version "1.1.2"
+  resolved "https://registry.yarnpkg.com/bin-links/-/bin-links-1.1.2.tgz#fb74bd54bae6b7befc6c6221f25322ac830d9757"
+  dependencies:
+    bluebird "^3.5.0"
+    cmd-shim "^2.0.2"
+    gentle-fs "^2.0.0"
+    graceful-fs "^4.1.11"
+    write-file-atomic "^2.3.0"
+
 binary-extensions@^1.0.0:
   version "1.10.0"
   resolved "https://registry.yarnpkg.com/binary-extensions/-/binary-extensions-1.10.0.tgz#9aeb9a6c5e88638aad171e167f5900abe24835d0"
@@ -1483,7 +1519,7 @@ bluebird@1.0.3:
   version "1.0.3"
   resolved "https://registry.yarnpkg.com/bluebird/-/bluebird-1.0.3.tgz#c4b441184802e3b64a61eeed4578271b4c8bf6ac"
 
-bluebird@^3.4.3, bluebird@^3.5.0:
+bluebird@^3.4.3, bluebird@^3.5.0, bluebird@^3.5.1, bluebird@~3.5.1:
   version "3.5.1"
   resolved "https://registry.yarnpkg.com/bluebird/-/bluebird-3.5.1.tgz#d9551f9de98f1fcda1e683d17ee91a0602ee2eb9"
 
@@ -1743,6 +1779,28 @@ builtins@^1.0.3:
   version "1.0.3"
   resolved "https://registry.yarnpkg.com/builtins/-/builtins-1.0.3.tgz#cb94faeb61c8696451db36534e1422f94f0aee88"
 
+byline@^5.0.0:
+  version "5.0.0"
+  resolved "https://registry.yarnpkg.com/byline/-/byline-5.0.0.tgz#741c5216468eadc457b03410118ad77de8c1ddb1"
+
+cacache@^10.0.0, cacache@^10.0.4:
+  version "10.0.4"
+  resolved "https://registry.yarnpkg.com/cacache/-/cacache-10.0.4.tgz#6452367999eff9d4188aefd9a14e9d7c6a263460"
+  dependencies:
+    bluebird "^3.5.1"
+    chownr "^1.0.1"
+    glob "^7.1.2"
+    graceful-fs "^4.1.11"
+    lru-cache "^4.1.1"
+    mississippi "^2.0.0"
+    mkdirp "^0.5.1"
+    move-concurrently "^1.0.1"
+    promise-inflight "^1.0.1"
+    rimraf "^2.6.2"
+    ssri "^5.2.4"
+    unique-filename "^1.1.0"
+    y18n "^4.0.0"
+
 cacache@^10.0.1:
   version "10.0.2"
   resolved "https://registry.yarnpkg.com/cacache/-/cacache-10.0.2.tgz#105a93a162bbedf3a25da42e1939ed99ffb145f8"
@@ -1761,6 +1819,10 @@ cacache@^10.0.1:
     unique-filename "^1.1.0"
     y18n "^3.2.1"
 
+call-limit@~1.1.0:
+  version "1.1.0"
+  resolved "https://registry.yarnpkg.com/call-limit/-/call-limit-1.1.0.tgz#6fd61b03f3da42a2cd0ec2b60f02bd0e71991fea"
+
 call-matcher@^1.0.1:
   version "1.0.1"
   resolved "https://registry.yarnpkg.com/call-matcher/-/call-matcher-1.0.1.tgz#5134d077984f712a54dad3cbf62de28dce416ca8"
@@ -1940,6 +2002,14 @@ chownr@^1.0.1, chownr@~1.0.1:
   version "1.0.1"
   resolved "https://registry.yarnpkg.com/chownr/-/chownr-1.0.1.tgz#e2a75042a9551908bebd25b8523d5f9769d79181"
 
+ci-info@^1.0.0:
+  version "1.1.3"
+  resolved "https://registry.yarnpkg.com/ci-info/-/ci-info-1.1.3.tgz#710193264bb05c77b8c90d02f5aaf22216a667b2"
+
+cidr-regex@1.0.6:
+  version "1.0.6"
+  resolved "https://registry.yarnpkg.com/cidr-regex/-/cidr-regex-1.0.6.tgz#74abfd619df370b9d54ab14475568e97dd64c0c1"
+
 cint@^8.2.1:
   version "8.2.1"
   resolved "https://registry.yarnpkg.com/cint/-/cint-8.2.1.tgz#70386b1b48e2773d0d63166a55aff94ef4456a12"
@@ -1961,7 +2031,7 @@ clap@^1.0.9:
   dependencies:
     chalk "^1.1.3"
 
-classnames@2.x, classnames@^2.1.2, classnames@^2.2.3, classnames@^2.2.4, classnames@^2.2.5:
+classnames@^2.2.3, classnames@^2.2.4, classnames@^2.2.5:
   version "2.2.5"
   resolved "https://registry.yarnpkg.com/classnames/-/classnames-2.2.5.tgz#fb3801d453467649ef3603c7d61a02bd129bde6d"
 
@@ -1987,6 +2057,15 @@ cli-cursor@^2.1.0:
   dependencies:
     restore-cursor "^2.0.0"
 
+cli-table2@~0.2.0:
+  version "0.2.0"
+  resolved "https://registry.yarnpkg.com/cli-table2/-/cli-table2-0.2.0.tgz#2d1ef7f218a0e786e214540562d4bd177fe32d97"
+  dependencies:
+    lodash "^3.10.1"
+    string-width "^1.0.1"
+  optionalDependencies:
+    colors "^1.1.2"
+
 cli-table@^0.3.1:
   version "0.3.1"
   resolved "https://registry.yarnpkg.com/cli-table/-/cli-table-0.3.1.tgz#f53b05266a8b1a0b934b3d0821e6e2dc5914ae23"
@@ -2027,6 +2106,14 @@ cliui@^3.2.0:
     strip-ansi "^3.0.1"
     wrap-ansi "^2.0.0"
 
+cliui@^4.0.0:
+  version "4.0.0"
+  resolved "https://registry.yarnpkg.com/cliui/-/cliui-4.0.0.tgz#743d4650e05f36d1ed2575b59638d87322bfbbcc"
+  dependencies:
+    string-width "^2.1.1"
+    strip-ansi "^4.0.0"
+    wrap-ansi "^2.0.0"
+
 clone-deep@^0.3.0:
   version "0.3.0"
   resolved "https://registry.yarnpkg.com/clone-deep/-/clone-deep-0.3.0.tgz#348c61ae9cdbe0edfe053d91ff4cc521d790ede8"
@@ -2044,7 +2131,7 @@ clone@^2.1.1:
   version "2.1.1"
   resolved "https://registry.yarnpkg.com/clone/-/clone-2.1.1.tgz#d217d1e961118e3ac9a4b8bba3285553bf647cdb"
 
-cmd-shim@~2.0.2:
+cmd-shim@^2.0.2, cmd-shim@~2.0.2:
   version "2.0.2"
   resolved "https://registry.yarnpkg.com/cmd-shim/-/cmd-shim-2.0.2.tgz#6fcbda99483a8fd15d7d30a196ca69d688a2efdb"
   dependencies:
@@ -2105,6 +2192,10 @@ colors@1.0.3:
   version "1.0.3"
   resolved "https://registry.yarnpkg.com/colors/-/colors-1.0.3.tgz#0433f44d809680fdeb60ed260f1b0c262e82a40b"
 
+colors@^1.1.2:
+  version "1.2.1"
+  resolved "https://registry.yarnpkg.com/colors/-/colors-1.2.1.tgz#f4a3d302976aaf042356ba1ade3b1a2c62d9d794"
+
 colors@~1.1.2:
   version "1.1.2"
   resolved "https://registry.yarnpkg.com/colors/-/colors-1.1.2.tgz#168a4701756b6a7f51a12ce0c97bfa28c084ed63"
@@ -2696,15 +2787,15 @@ debug@2.6.8:
   dependencies:
     ms "2.0.0"
 
-debug@^2.1.2, debug@^2.2.0, debug@^2.6.3, debug@^2.6.8:
-  version "2.6.9"
-  resolved "https://registry.yarnpkg.com/debug/-/debug-2.6.9.tgz#5d128515df134ff327e90a4c93f4e077a536341f"
+debug@3.1.0, debug@^3.1.0:
+  version "3.1.0"
+  resolved "https://registry.yarnpkg.com/debug/-/debug-3.1.0.tgz#5bb5a0672628b64149566ba16819e61518c67261"
   dependencies:
     ms "2.0.0"
 
-debug@^3.1.0:
-  version "3.1.0"
-  resolved "https://registry.yarnpkg.com/debug/-/debug-3.1.0.tgz#5bb5a0672628b64149566ba16819e61518c67261"
+debug@^2.1.2, debug@^2.2.0, debug@^2.6.3, debug@^2.6.8:
+  version "2.6.9"
+  resolved "https://registry.yarnpkg.com/debug/-/debug-2.6.9.tgz#5d128515df134ff327e90a4c93f4e077a536341f"
   dependencies:
     ms "2.0.0"
 
@@ -2734,6 +2825,10 @@ deck.gl@^5.1.4:
     seer "^0.2.4"
     viewport-mercator-project "^5.0.0"
 
+decode-uri-component@^0.2.0:
+  version "0.2.0"
+  resolved "https://registry.yarnpkg.com/decode-uri-component/-/decode-uri-component-0.2.0.tgz#eb3913333458775cb84cd1a1fae062106bb87545"
+
 deep-eql@^3.0.0:
   version "3.0.1"
   resolved "https://registry.yarnpkg.com/deep-eql/-/deep-eql-3.0.1.tgz#dfc9404400ad1c8fe023e7da1df1c147c4b444df"
@@ -2816,6 +2911,14 @@ detect-indent@^4.0.0:
   dependencies:
     repeating "^2.0.0"
 
+detect-indent@~5.0.0:
+  version "5.0.0"
+  resolved "https://registry.yarnpkg.com/detect-indent/-/detect-indent-5.0.0.tgz#3871cc0a6a002e8c3e5b3cf7f336264675f06b9d"
+
+detect-newline@^2.1.0:
+  version "2.1.0"
+  resolved "https://registry.yarnpkg.com/detect-newline/-/detect-newline-2.1.0.tgz#f41f1c10be4b00e87b5f13da680759f2c5bfd3e2"
+
 dezalgo@^1.0.0, dezalgo@^1.0.1, dezalgo@~1.0.3:
   version "1.0.3"
   resolved "https://registry.yarnpkg.com/dezalgo/-/dezalgo-1.0.3.tgz#7f742de066fc748bc8db820569dddce49bf0d456"
@@ -2839,12 +2942,25 @@ diffie-hellman@^5.0.0:
     miller-rabin "^4.0.0"
     randombytes "^2.0.0"
 
+disposables@^1.0.1:
+  version "1.0.2"
+  resolved "https://registry.yarnpkg.com/disposables/-/disposables-1.0.2.tgz#36c6a674475f55a2d6913567a601444e487b4b6e"
+
 distributions@^1.0.0:
   version "1.0.0"
   resolved "https://registry.yarnpkg.com/distributions/-/distributions-1.0.0.tgz#16466e676df7f311929941d3d7f02010466671a9"
   dependencies:
     mathfn "^1.0.0"
 
+dnd-core@^2.6.0:
+  version "2.6.0"
+  resolved "https://registry.yarnpkg.com/dnd-core/-/dnd-core-2.6.0.tgz#12bad66d58742c6e5f7cf2943fb6859440f809c4"
+  dependencies:
+    asap "^2.0.6"
+    invariant "^2.0.0"
+    lodash "^4.2.0"
+    redux "^3.7.1"
+
 doctrine@1.5.0:
   version "1.5.0"
   resolved "https://registry.yarnpkg.com/doctrine/-/doctrine-1.5.0.tgz#379dce730f6166f76cefa4e6707a159b02c5a6fa"
@@ -2924,6 +3040,10 @@ dot-prop@^4.1.0:
   dependencies:
     is-obj "^1.0.0"
 
+dotenv@^5.0.1:
+  version "5.0.1"
+  resolved "https://registry.yarnpkg.com/dotenv/-/dotenv-5.0.1.tgz#a5317459bd3d79ab88cff6e44057a6a3fbb1fcef"
+
 duplexer2@^0.1.4:
   version "0.1.4"
   resolved "https://registry.yarnpkg.com/duplexer2/-/duplexer2-0.1.4.tgz#8b12dab878c0d69e3e7891051662a32fc6bddcc1"
@@ -2980,10 +3100,6 @@ electron-to-chromium@^1.2.7, electron-to-chromium@^1.3.18:
   version "1.3.24"
   resolved "https://registry.yarnpkg.com/electron-to-chromium/-/electron-to-chromium-1.3.24.tgz#9b7b88bb05ceb9fa016a177833cc2dde388f21b6"
 
-element-class@^0.2.0:
-  version "0.2.2"
-  resolved "https://registry.yarnpkg.com/element-class/-/element-class-0.2.2.tgz#9d3bbd0767f9013ef8e1c8ebe722c1402a60050e"
-
 elliptic@^6.0.0:
   version "6.4.0"
   resolved "https://registry.yarnpkg.com/elliptic/-/elliptic-6.4.0.tgz#cac9af8762c85836187003c8dfe193e5e2eae5df"
@@ -3048,6 +3164,10 @@ enzyme@^2.0.0:
     prop-types "^15.5.10"
     uuid "^3.0.1"
 
+err-code@^1.0.0:
+  version "1.1.2"
+  resolved "https://registry.yarnpkg.com/err-code/-/err-code-1.1.2.tgz#06e0116d3028f6aef4806849eb0ea6a748ae6960"
+
 errno@^0.1.1, errno@^0.1.3:
   version "0.1.4"
   resolved "https://registry.yarnpkg.com/errno/-/errno-0.1.4.tgz#b896e23a9e5e8ba33871fc996abd3635fc9a1c7d"
@@ -3060,6 +3180,12 @@ errno@^0.1.4:
   dependencies:
     prr "~1.0.1"
 
+errno@~0.1.7:
+  version "0.1.7"
+  resolved "https://registry.yarnpkg.com/errno/-/errno-0.1.7.tgz#4684d71779ad39af177e3f007996f7c67c852618"
+  dependencies:
+    prr "~1.0.1"
+
 error-ex@^1.2.0:
   version "1.3.1"
   resolved "https://registry.yarnpkg.com/error-ex/-/error-ex-1.3.1.tgz#f855a86ce61adc4e8621c3cda21e7a7612c3a8dc"
@@ -3114,10 +3240,16 @@ es6-promise@^3.0.2, es6-promise@^3.1.2:
   version "3.3.1"
   resolved "https://registry.yarnpkg.com/es6-promise/-/es6-promise-3.3.1.tgz#a08cdde84ccdbf34d027a1451bc91d4bcd28a613"
 
-es6-promise@^4.1.1:
+es6-promise@^4.0.3, es6-promise@^4.1.1:
   version "4.2.4"
   resolved "https://registry.yarnpkg.com/es6-promise/-/es6-promise-4.2.4.tgz#dc4221c2b16518760bd8c39a52d8f356fc00ed29"
 
+es6-promisify@^5.0.0:
+  version "5.0.0"
+  resolved "https://registry.yarnpkg.com/es6-promisify/-/es6-promisify-5.0.0.tgz#5109d62f3e56ea967c4b63505aef08291c8a5203"
+  dependencies:
+    es6-promise "^4.0.3"
+
 es6-set@~0.1.5:
   version "0.1.5"
   resolved "https://registry.yarnpkg.com/es6-set/-/es6-set-0.1.5.tgz#d2b3ec5d4d800ced818db538d28974db0a73ccb1"
@@ -3214,6 +3346,12 @@ eslint-config-airbnb@^15.0.1:
   dependencies:
     eslint-config-airbnb-base "^11.3.0"
 
+eslint-config-prettier@^2.9.0:
+  version "2.9.0"
+  resolved "https://registry.yarnpkg.com/eslint-config-prettier/-/eslint-config-prettier-2.9.0.tgz#5ecd65174d486c22dff389fe036febf502d468a3"
+  dependencies:
+    get-stdin "^5.0.1"
+
 eslint-import-resolver-node@^0.3.1:
   version "0.3.1"
   resolved "https://registry.yarnpkg.com/eslint-import-resolver-node/-/eslint-import-resolver-node-0.3.1.tgz#4422574cde66a9a7b099938ee4d508a199e0e3cc"
@@ -3255,6 +3393,13 @@ eslint-plugin-jsx-a11y@^5.1.1:
     emoji-regex "^6.1.0"
     jsx-ast-utils "^1.4.0"
 
+eslint-plugin-prettier@^2.6.0:
+  version "2.6.0"
+  resolved "https://registry.yarnpkg.com/eslint-plugin-prettier/-/eslint-plugin-prettier-2.6.0.tgz#33e4e228bdb06142d03c560ce04ec23f6c767dd7"
+  dependencies:
+    fast-diff "^1.1.1"
+    jest-docblock "^21.0.0"
+
 eslint-plugin-react@^7.0.1:
   version "7.4.0"
   resolved "https://registry.yarnpkg.com/eslint-plugin-react/-/eslint-plugin-react-7.4.0.tgz#300a95861b9729c087d362dd64abcc351a74364a"
@@ -3422,10 +3567,6 @@ execa@^0.7.0:
     signal-exit "^3.0.0"
     strip-eof "^1.0.0"
 
-exenv@1.2.0:
-  version "1.2.0"
-  resolved "https://registry.yarnpkg.com/exenv/-/exenv-1.2.0.tgz#3835f127abf075bfe082d0aed4484057c78e3c89"
-
 exit-hook@^1.0.0:
   version "1.1.1"
   resolved "https://registry.yarnpkg.com/exit-hook/-/exit-hook-1.1.1.tgz#f05ca233b48c05d54fff07765df8507e95c02ff8"
@@ -3507,7 +3648,7 @@ fast-deep-equal@^1.0.0:
   version "1.0.0"
   resolved "https://registry.yarnpkg.com/fast-deep-equal/-/fast-deep-equal-1.0.0.tgz#96256a3bc975595eb36d82e9929d060d893439ff"
 
-fast-diff@^1.0.1:
+fast-diff@^1.0.1, fast-diff@^1.1.1:
   version "1.1.2"
   resolved "https://registry.yarnpkg.com/fast-diff/-/fast-diff-1.1.2.tgz#4b62c42b8e03de3f848460b639079920695d0154"
 
@@ -3604,6 +3745,10 @@ find-cache-dir@^1.0.0:
     make-dir "^1.0.0"
     pkg-dir "^2.0.0"
 
+find-npm-prefix@^1.0.2:
+  version "1.0.2"
+  resolved "https://registry.yarnpkg.com/find-npm-prefix/-/find-npm-prefix-1.0.2.tgz#8d8ce2c78b3b4b9e66c8acc6a37c231eb841cfdf"
+
 find-up@1.1.2, find-up@^1.0.0:
   version "1.1.2"
   resolved "https://registry.yarnpkg.com/find-up/-/find-up-1.1.2.tgz#6b2e9822b1a2ce0a60ab64d610eccad53cb24d0f"
@@ -3718,6 +3863,13 @@ fraction.js@4.0.4:
   version "4.0.4"
   resolved "https://registry.yarnpkg.com/fraction.js/-/fraction.js-4.0.4.tgz#04e567110718adf7b52974a10434ab4c67a5183e"
 
+from2@^1.3.0:
+  version "1.3.0"
+  resolved "https://registry.yarnpkg.com/from2/-/from2-1.3.0.tgz#88413baaa5f9a597cfde9221d86986cd3c061dfd"
+  dependencies:
+    inherits "~2.0.1"
+    readable-stream "~1.1.10"
+
 from2@^2.1.0:
   version "2.3.0"
   resolved "https://registry.yarnpkg.com/from2/-/from2-2.3.0.tgz#8bfb5502bde4a4d36cfdeea007fcca21d7e382af"
@@ -3735,11 +3887,17 @@ fs-extra@^0.30.0:
     path-is-absolute "^1.0.0"
     rimraf "^2.2.8"
 
+fs-minipass@^1.2.5:
+  version "1.2.5"
+  resolved "https://registry.yarnpkg.com/fs-minipass/-/fs-minipass-1.2.5.tgz#06c277218454ec288df77ada54a03b8702aacb9d"
+  dependencies:
+    minipass "^2.2.1"
+
 fs-readdir-recursive@^1.0.0:
   version "1.0.0"
   resolved "https://registry.yarnpkg.com/fs-readdir-recursive/-/fs-readdir-recursive-1.0.0.tgz#8cd1745c8b4f8a29c8caec392476921ba195f560"
 
-fs-vacuum@~1.2.9:
+fs-vacuum@^1.2.10, fs-vacuum@~1.2.10, fs-vacuum@~1.2.9:
   version "1.2.10"
   resolved "https://registry.yarnpkg.com/fs-vacuum/-/fs-vacuum-1.2.10.tgz#b7629bec07a4031a2548fdf99f5ecf1cc8b31e36"
   dependencies:
@@ -3747,7 +3905,7 @@ fs-vacuum@~1.2.9:
     path-is-inside "^1.0.1"
     rimraf "^2.5.2"
 
-fs-write-stream-atomic@^1.0.8, fs-write-stream-atomic@~1.0.8:
+fs-write-stream-atomic@^1.0.8, fs-write-stream-atomic@~1.0.10, fs-write-stream-atomic@~1.0.8:
   version "1.0.10"
   resolved "https://registry.yarnpkg.com/fs-write-stream-atomic/-/fs-write-stream-atomic-1.0.10.tgz#b47df53493ef911df75731e70a9ded0189db40c9"
   dependencies:
@@ -3807,6 +3965,10 @@ functional-red-black-tree@^1.0.1:
   version "1.0.1"
   resolved "https://registry.yarnpkg.com/functional-red-black-tree/-/functional-red-black-tree-1.0.1.tgz#1b0ab3bd553b2a0d6399d29c0e3ea0b252078327"
 
+fuse.js@^3.0.0:
+  version "3.2.0"
+  resolved "https://registry.yarnpkg.com/fuse.js/-/fuse.js-3.2.0.tgz#f0448e8069855bf2a3e683cdc1d320e7e2a07ef4"
+
 gauge@~2.6.0:
   version "2.6.0"
   resolved "https://registry.yarnpkg.com/gauge/-/gauge-2.6.0.tgz#d35301ad18e96902b4751dcbbe40f4218b942a46"
@@ -3850,6 +4012,23 @@ generic-names@^1.0.1:
   dependencies:
     loader-utils "^0.2.16"
 
+genfun@^4.0.1:
+  version "4.0.1"
+  resolved "https://registry.yarnpkg.com/genfun/-/genfun-4.0.1.tgz#ed10041f2e4a7f1b0a38466d17a5c3e27df1dfc1"
+
+gentle-fs@^2.0.0, gentle-fs@^2.0.1:
+  version "2.0.1"
+  resolved "https://registry.yarnpkg.com/gentle-fs/-/gentle-fs-2.0.1.tgz#585cfd612bfc5cd52471fdb42537f016a5ce3687"
+  dependencies:
+    aproba "^1.1.2"
+    fs-vacuum "^1.2.10"
+    graceful-fs "^4.1.11"
+    iferr "^0.1.5"
+    mkdirp "^0.5.1"
+    path-is-inside "^1.0.2"
+    read-cmd-shim "^1.0.1"
+    slide "^1.1.6"
+
 geojson-area@0.1.0:
   version "0.1.0"
   resolved "https://registry.yarnpkg.com/geojson-area/-/geojson-area-0.1.0.tgz#d48d807082cfadf4a78df1349be50f38bf1894ae"
@@ -4064,7 +4243,7 @@ glob@7.1.1:
     once "^1.3.0"
     path-is-absolute "^1.0.0"
 
-glob@^7.0.3, glob@^7.0.5, glob@^7.1.1, glob@^7.1.2, glob@~7.1.0:
+glob@^7.0.3, glob@^7.0.5, glob@^7.1.1, glob@^7.1.2, glob@~7.1.0, glob@~7.1.2:
   version "7.1.2"
   resolved "https://registry.yarnpkg.com/glob/-/glob-7.1.2.tgz#c19c9df9a028702d678612384a6552404c636d15"
   dependencies:
@@ -4158,7 +4337,7 @@ got@^6.7.1:
     unzip-response "^2.0.1"
     url-parse-lax "^1.0.0"
 
-graceful-fs@^4.1.11, graceful-fs@^4.1.2, graceful-fs@^4.1.4, graceful-fs@^4.1.6, graceful-fs@^4.1.9, graceful-fs@~4.1.9:
+graceful-fs@^4.1.11, graceful-fs@^4.1.2, graceful-fs@^4.1.4, graceful-fs@^4.1.6, graceful-fs@^4.1.9, graceful-fs@~4.1.11, graceful-fs@~4.1.9:
   version "4.1.11"
   resolved "https://registry.yarnpkg.com/graceful-fs/-/graceful-fs-4.1.11.tgz#0e8bdfe4d1ddb8854d64e04ea7c00e2a026e5658"
 
@@ -4355,6 +4534,10 @@ hoist-non-react-statics@^1.0.0, hoist-non-react-statics@^1.2.0:
   version "1.2.0"
   resolved "https://registry.yarnpkg.com/hoist-non-react-statics/-/hoist-non-react-statics-1.2.0.tgz#aa448cf0986d55cc40773b17174b7dd066cb7cfb"
 
+hoist-non-react-statics@^2.1.0:
+  version "2.5.0"
+  resolved "https://registry.yarnpkg.com/hoist-non-react-statics/-/hoist-non-react-statics-2.5.0.tgz#d2ca2dfc19c5a91c5a6615ce8e564ef0347e2a40"
+
 hoist-non-react-statics@^2.2.1:
   version "2.3.1"
   resolved "https://registry.yarnpkg.com/hoist-non-react-statics/-/hoist-non-react-statics-2.3.1.tgz#343db84c6018c650778898240135a1420ee22ce0"
@@ -4370,7 +4553,7 @@ hosted-git-info@^2.1.4:
   version "2.5.0"
   resolved "https://registry.yarnpkg.com/hosted-git-info/-/hosted-git-info-2.5.0.tgz#6d60e34b3abbc8313062c3b798ef8d901a07af3c"
 
-hosted-git-info@^2.1.5, hosted-git-info@^2.4.2:
+hosted-git-info@^2.1.5, hosted-git-info@^2.4.2, hosted-git-info@^2.5.0, hosted-git-info@^2.6.0:
   version "2.6.0"
   resolved "https://registry.yarnpkg.com/hosted-git-info/-/hosted-git-info-2.6.0.tgz#23235b29ab230c576aab0d4f13fc046b0b038222"
 
@@ -4407,6 +4590,17 @@ htmlparser2@^3.9.1:
     inherits "^2.0.1"
     readable-stream "^2.0.2"
 
+http-cache-semantics@^3.8.0:
+  version "3.8.1"
+  resolved "https://registry.yarnpkg.com/http-cache-semantics/-/http-cache-semantics-3.8.1.tgz#39b0e16add9b605bf0a9ef3d9daaf4843b4cacd2"
+
+http-proxy-agent@^2.0.0:
+  version "2.1.0"
+  resolved "https://registry.yarnpkg.com/http-proxy-agent/-/http-proxy-agent-2.1.0.tgz#e4821beef5b2142a2026bd73926fe537631c5405"
+  dependencies:
+    agent-base "4"
+    debug "3.1.0"
+
 http-signature@~0.10.0:
   version "0.10.1"
   resolved "https://registry.yarnpkg.com/http-signature/-/http-signature-0.10.1.tgz#4fbdac132559aa8323121e540779c0a012b27e66"
@@ -4435,6 +4629,19 @@ https-browserify@0.0.1:
   version "0.0.1"
   resolved "https://registry.yarnpkg.com/https-browserify/-/https-browserify-0.0.1.tgz#3f91365cabe60b77ed0ebba24b454e3e09d95a82"
 
+https-proxy-agent@^2.1.0:
+  version "2.2.1"
+  resolved "https://registry.yarnpkg.com/https-proxy-agent/-/https-proxy-agent-2.2.1.tgz#51552970fa04d723e04c56d04178c3f92592bbc0"
+  dependencies:
+    agent-base "^4.1.0"
+    debug "^3.1.0"
+
+humanize-ms@^1.2.1:
+  version "1.2.1"
+  resolved "https://registry.yarnpkg.com/humanize-ms/-/humanize-ms-1.2.1.tgz#c46e3159a293f6b896da29316d8b6fe8bb79bbed"
+  dependencies:
+    ms "^2.0.0"
+
 hyperquest@~1.2.0:
   version "1.2.0"
   resolved "https://registry.yarnpkg.com/hyperquest/-/hyperquest-1.2.0.tgz#39e1fef66888dc7ce0dec6c0dd814f6fc8944ad5"
@@ -4486,6 +4693,12 @@ ignore-styles@^5.0.1:
   version "5.0.1"
   resolved "https://registry.yarnpkg.com/ignore-styles/-/ignore-styles-5.0.1.tgz#b49ef2274bdafcd8a4880a966bfe38d1a0bf4671"
 
+ignore-walk@^3.0.1:
+  version "3.0.1"
+  resolved "https://registry.yarnpkg.com/ignore-walk/-/ignore-walk-3.0.1.tgz#a83e62e7d272ac0e3b551aaa82831a19b69f82f8"
+  dependencies:
+    minimatch "^3.0.4"
+
 ignore@^3.3.3:
   version "3.3.7"
   resolved "https://registry.yarnpkg.com/ignore/-/ignore-3.3.7.tgz#612289bfb3c220e186a58118618d5be8c1bab021"
@@ -4525,7 +4738,7 @@ infinity-agent@^2.0.0:
   version "2.0.3"
   resolved "https://registry.yarnpkg.com/infinity-agent/-/infinity-agent-2.0.3.tgz#45e0e2ff7a9eb030b27d62b74b3744b7a7ac4216"
 
-inflight@^1.0.4, inflight@~1.0.5:
+inflight@^1.0.4, inflight@~1.0.5, inflight@~1.0.6:
   version "1.0.6"
   resolved "https://registry.yarnpkg.com/inflight/-/inflight-1.0.6.tgz#49bd6331d7d02d0c09bc910a1075ba8165b56df9"
   dependencies:
@@ -4540,7 +4753,7 @@ inherits@2.0.1:
   version "2.0.1"
   resolved "https://registry.yarnpkg.com/inherits/-/inherits-2.0.1.tgz#b17d08d326b4423e568eff719f91b0b1cbdf69f1"
 
-ini@1.x.x, ini@^1.3.4, ini@~1.3.4:
+ini@1.x.x, ini@^1.3.4, ini@^1.3.5, ini@~1.3.4:
   version "1.3.5"
   resolved "https://registry.yarnpkg.com/ini/-/ini-1.3.5.tgz#eee25f56db1c9ec6085e0c22778083f596abf927"
 
@@ -4548,6 +4761,19 @@ ini@~1.3.0:
   version "1.3.4"
   resolved "https://registry.yarnpkg.com/ini/-/ini-1.3.4.tgz#0537cb79daf59b59a1a517dff706c86ec039162e"
 
+init-package-json@^1.10.3:
+  version "1.10.3"
+  resolved "https://registry.yarnpkg.com/init-package-json/-/init-package-json-1.10.3.tgz#45ffe2f610a8ca134f2bd1db5637b235070f6cbe"
+  dependencies:
+    glob "^7.1.1"
+    npm-package-arg "^4.0.0 || ^5.0.0 || ^6.0.0"
+    promzard "^0.3.0"
+    read "~1.0.1"
+    read-package-json "1 || 2"
+    semver "2.x || 3.x || 4 || 5"
+    validate-npm-package-license "^3.0.1"
+    validate-npm-package-name "^3.0.0"
+
 init-package-json@~1.9.4:
   version "1.9.6"
   resolved "https://registry.yarnpkg.com/init-package-json/-/init-package-json-1.9.6.tgz#789fc2b74466a4952b9ea77c0575bc78ebd60a61"
@@ -4619,6 +4845,10 @@ invert-kv@^1.0.0:
   version "1.0.0"
   resolved "https://registry.yarnpkg.com/invert-kv/-/invert-kv-1.0.0.tgz#104a8e4aaca6d3d8cd157a8ef8bfab2d7a3ffdb6"
 
+ip@^1.1.4:
+  version "1.1.5"
+  resolved "https://registry.yarnpkg.com/ip/-/ip-1.1.5.tgz#bdded70114290828c0a039e72ef25f5aaec4354a"
+
 is-absolute-url@^2.0.0:
   version "2.1.0"
   resolved "https://registry.yarnpkg.com/is-absolute-url/-/is-absolute-url-2.1.0.tgz#50530dfb84fcc9aa7dbe7852e83a37b93b9f2aa6"
@@ -4651,6 +4881,18 @@ is-callable@^1.1.1, is-callable@^1.1.3:
   version "1.1.3"
   resolved "https://registry.yarnpkg.com/is-callable/-/is-callable-1.1.3.tgz#86eb75392805ddc33af71c92a0eedf74ee7604b2"
 
+is-ci@^1.0.10:
+  version "1.1.0"
+  resolved "https://registry.yarnpkg.com/is-ci/-/is-ci-1.1.0.tgz#247e4162e7860cebbdaf30b774d6b0ac7dcfe7a5"
+  dependencies:
+    ci-info "^1.0.0"
+
+is-cidr@~1.0.0:
+  version "1.0.0"
+  resolved "https://registry.yarnpkg.com/is-cidr/-/is-cidr-1.0.0.tgz#fb5aacf659255310359da32cae03e40c6a1c2afc"
+  dependencies:
+    cidr-regex "1.0.6"
+
 is-date-object@^1.0.1:
   version "1.0.1"
   resolved "https://registry.yarnpkg.com/is-date-object/-/is-date-object-1.0.1.tgz#9aa20eb6aeebbff77fbd33e74ca01b33581d3a16"
@@ -4948,6 +5190,10 @@ jed@^1.1.1:
   version "1.1.1"
   resolved "https://registry.yarnpkg.com/jed/-/jed-1.1.1.tgz#7a549bbd9ffe1585b0cd0a191e203055bee574b4"
 
+jest-docblock@^21.0.0:
+  version "21.2.0"
+  resolved "https://registry.yarnpkg.com/jest-docblock/-/jest-docblock-21.2.0.tgz#51529c3b30d5fd159da60c27ceedc195faf8d414"
+
 jju@^1.1.0:
   version "1.3.0"
   resolved "https://registry.yarnpkg.com/jju/-/jju-1.3.0.tgz#dadd9ef01924bc728b03f2f7979bdbd62f7a2aaa"
@@ -5033,6 +5279,10 @@ json-loader@^0.5.4:
   version "0.5.7"
   resolved "https://registry.yarnpkg.com/json-loader/-/json-loader-0.5.7.tgz#dca14a70235ff82f0ac9a3abeb60d337a365185d"
 
+json-parse-better-errors@^1.0.0:
+  version "1.0.2"
+  resolved "https://registry.yarnpkg.com/json-parse-better-errors/-/json-parse-better-errors-1.0.2.tgz#bb867cfb3450e69107c131d1c514bab3dc8bcaa9"
+
 json-parse-better-errors@^1.0.1:
   version "1.0.1"
   resolved "https://registry.yarnpkg.com/json-parse-better-errors/-/json-parse-better-errors-1.0.1.tgz#50183cd1b2d25275de069e9e71b467ac9eab973a"
@@ -5094,6 +5344,10 @@ jsonlint-lines-primitives@~1.6.0:
     JSV ">= 4.0.x"
     nomnom ">= 1.5.x"
 
+jsonparse@^1.2.0:
+  version "1.3.1"
+  resolved "https://registry.yarnpkg.com/jsonparse/-/jsonparse-1.3.1.tgz#3f4dae4a91fac315f71062f8521cc239f1366280"
+
 jsonpointer@^4.0.0:
   version "4.0.1"
   resolved "https://registry.yarnpkg.com/jsonpointer/-/jsonpointer-4.0.1.tgz#4fd92cb34e0e9db3c89c8622ecf51f9b978c6cb9"
@@ -5183,6 +5437,10 @@ lazy-cache@^1.0.3:
   version "1.0.4"
   resolved "https://registry.yarnpkg.com/lazy-cache/-/lazy-cache-1.0.4.tgz#a1d78fc3a50474cb80845d3b3b6e1da49a446e8e"
 
+lazy-property@~1.0.0:
+  version "1.0.0"
+  resolved "https://registry.yarnpkg.com/lazy-property/-/lazy-property-1.0.0.tgz#84ddc4b370679ba8bd4cdcfa4c06b43d57111147"
+
 lcid@^1.0.0:
   version "1.0.0"
   resolved "https://registry.yarnpkg.com/lcid/-/lcid-1.0.0.tgz#308accafa0bc483a3867b4b6f2b9506251d1b835"
@@ -5217,6 +5475,37 @@ levn@^0.3.0, levn@~0.3.0:
     prelude-ls "~1.1.2"
     type-check "~0.3.2"
 
+libcipm@^1.6.0:
+  version "1.6.2"
+  resolved "https://registry.yarnpkg.com/libcipm/-/libcipm-1.6.2.tgz#5a9d83b8606b9733cfff016ad9b37d3b8198ae09"
+  dependencies:
+    bin-links "^1.1.0"
+    bluebird "^3.5.1"
+    find-npm-prefix "^1.0.2"
+    graceful-fs "^4.1.11"
+    lock-verify "^2.0.0"
+    npm-lifecycle "^2.0.0"
+    npm-logical-tree "^1.2.1"
+    npm-package-arg "^6.0.0"
+    pacote "^7.5.1"
+    protoduck "^5.0.0"
+    read-package-json "^2.0.12"
+    rimraf "^2.6.2"
+    worker-farm "^1.5.4"
+
+libnpx@^10.0.1:
+  version "10.2.0"
+  resolved "https://registry.yarnpkg.com/libnpx/-/libnpx-10.2.0.tgz#1bf4a1c9f36081f64935eb014041da10855e3102"
+  dependencies:
+    dotenv "^5.0.1"
+    npm-package-arg "^6.0.0"
+    rimraf "^2.6.2"
+    safe-buffer "^5.1.0"
+    update-notifier "^2.3.0"
+    which "^1.3.0"
+    y18n "^4.0.0"
+    yargs "^11.0.0"
+
 load-json-file@^1.0.0:
   version "1.1.0"
   resolved "https://registry.yarnpkg.com/load-json-file/-/load-json-file-1.1.0.tgz#956905708d58b4bab4c2261b04f59f31c99374c0"
@@ -5264,10 +5553,23 @@ locate-path@^2.0.0:
     p-locate "^2.0.0"
     path-exists "^3.0.0"
 
+lock-verify@^2.0.0:
+  version "2.0.1"
+  resolved "https://registry.yarnpkg.com/lock-verify/-/lock-verify-2.0.1.tgz#6d671eea60b459c6048b3b26b62959208be67682"
+  dependencies:
+    npm-package-arg "^5.1.2"
+    semver "^5.4.1"
+
 lockfile@~1.0.2:
   version "1.0.3"
   resolved "https://registry.yarnpkg.com/lockfile/-/lockfile-1.0.3.tgz#2638fc39a0331e9cac1a04b71799931c9c50df79"
 
+lockfile@~1.0.3:
+  version "1.0.4"
+  resolved "https://registry.yarnpkg.com/lockfile/-/lockfile-1.0.4.tgz#07f819d25ae48f87e538e6578b6964a4981a5609"
+  dependencies:
+    signal-exit "^3.0.2"
+
 lodash-es@^4.2.0, lodash-es@^4.2.1:
   version "4.17.4"
   resolved "https://registry.yarnpkg.com/lodash-es/-/lodash-es-4.17.4.tgz#dcc1d7552e150a0640073ba9cb31d70f032950e7"
@@ -5310,7 +5612,7 @@ lodash._root@~3.0.0:
   version "3.0.1"
   resolved "https://registry.yarnpkg.com/lodash._root/-/lodash._root-3.0.1.tgz#fba1c4524c19ee9a5f8136b4609f017cf4ded692"
 
-lodash.assign@^4.0.3, lodash.assign@^4.0.6, lodash.assign@^4.2.0:
+lodash.assign@^4.0.3, lodash.assign@^4.0.6:
   version "4.2.0"
   resolved "https://registry.yarnpkg.com/lodash.assign/-/lodash.assign-4.2.0.tgz#0d99f3ccd7a6d261d19bdaeb9245005d285808e7"
 
@@ -5378,7 +5680,7 @@ lodash.isarray@^3.0.0:
   version "3.0.4"
   resolved "https://registry.yarnpkg.com/lodash.isarray/-/lodash.isarray-3.0.4.tgz#79e4eb88c36a8122af86f844aa9bcd851b5fbb55"
 
-lodash.isequal@^4.0.0, lodash.isequal@^4.1.1:
+lodash.isequal@^4.1.1:
   version "4.5.0"
   resolved "https://registry.yarnpkg.com/lodash.isequal/-/lodash.isequal-4.5.0.tgz#415c4478f2bcc30120c22ce10ed3226f7d3e18e0"
 
@@ -5442,7 +5744,7 @@ lodash@2.4.1:
   version "2.4.1"
   resolved "https://registry.yarnpkg.com/lodash/-/lodash-2.4.1.tgz#5b7723034dda4d262e5a46fb2c58d7cc22f71420"
 
-lodash@3.x:
+lodash@3.x, lodash@^3.10.1:
   version "3.10.1"
   resolved "https://registry.yarnpkg.com/lodash/-/lodash-3.10.1.tgz#5bf45e8e49ba4189e17d482789dfd15bd140b7b6"
 
@@ -5482,7 +5784,7 @@ lowlight@~1.9.1:
   dependencies:
     highlight.js "~9.12.0"
 
-lru-cache@^4.0.0:
+lru-cache@^4.0.0, lru-cache@~4.1.1:
   version "4.1.2"
   resolved "https://registry.yarnpkg.com/lru-cache/-/lru-cache-4.1.2.tgz#45234b2e6e2f2b33da125624c4664929a0224c3f"
   dependencies:
@@ -5520,6 +5822,22 @@ make-dir@^1.0.0:
   dependencies:
     pify "^2.3.0"
 
+make-fetch-happen@^2.5.0, make-fetch-happen@^2.6.0:
+  version "2.6.0"
+  resolved "https://registry.yarnpkg.com/make-fetch-happen/-/make-fetch-happen-2.6.0.tgz#8474aa52198f6b1ae4f3094c04e8370d35ea8a38"
+  dependencies:
+    agentkeepalive "^3.3.0"
+    cacache "^10.0.0"
+    http-cache-semantics "^3.8.0"
+    http-proxy-agent "^2.0.0"
+    https-proxy-agent "^2.1.0"
+    lru-cache "^4.1.1"
+    mississippi "^1.2.0"
+    node-fetch-npm "^2.0.2"
+    promise-retry "^1.1.1"
+    socks-proxy-agent "^3.0.1"
+    ssri "^5.0.0"
+
 mapbox-gl-supported@^1.2.0:
   version "1.2.0"
   resolved "https://registry.yarnpkg.com/mapbox-gl-supported/-/mapbox-gl-supported-1.2.0.tgz#cbd34df894206cadda9a33c8d9a4609f26bb1989"
@@ -5638,6 +5956,10 @@ md5@^2.1.0:
     crypt "~0.0.1"
     is-buffer "~1.1.1"
 
+meant@~1.0.1:
+  version "1.0.1"
+  resolved "https://registry.yarnpkg.com/meant/-/meant-1.0.1.tgz#66044fea2f23230ec806fb515efea29c44d2115d"
+
 mem@^1.1.0:
   version "1.1.0"
   resolved "https://registry.yarnpkg.com/mem/-/mem-1.1.0.tgz#5edd52b485ca1d900fe64895505399a0dfa45f76"
@@ -5742,6 +6064,34 @@ minimist@~0.0.1:
   version "0.0.10"
   resolved "https://registry.yarnpkg.com/minimist/-/minimist-0.0.10.tgz#de3f98543dbf96082be48ad1a0c7cda836301dcf"
 
+minipass@^2.2.1, minipass@^2.2.4:
+  version "2.2.4"
+  resolved "https://registry.yarnpkg.com/minipass/-/minipass-2.2.4.tgz#03c824d84551ec38a8d1bb5bc350a5a30a354a40"
+  dependencies:
+    safe-buffer "^5.1.1"
+    yallist "^3.0.0"
+
+minizlib@^1.1.0:
+  version "1.1.0"
+  resolved "https://registry.yarnpkg.com/minizlib/-/minizlib-1.1.0.tgz#11e13658ce46bc3a70a267aac58359d1e0c29ceb"
+  dependencies:
+    minipass "^2.2.1"
+
+mississippi@^1.2.0:
+  version "1.3.1"
+  resolved "https://registry.yarnpkg.com/mississippi/-/mississippi-1.3.1.tgz#2a8bb465e86550ac8b36a7b6f45599171d78671e"
+  dependencies:
+    concat-stream "^1.5.0"
+    duplexify "^3.4.2"
+    end-of-stream "^1.1.0"
+    flush-write-stream "^1.0.0"
+    from2 "^2.1.0"
+    parallel-transform "^1.1.0"
+    pump "^1.0.0"
+    pumpify "^1.3.3"
+    stream-each "^1.1.0"
+    through2 "^2.0.0"
+
 mississippi@^1.3.0:
   version "1.3.0"
   resolved "https://registry.yarnpkg.com/mississippi/-/mississippi-1.3.0.tgz#d201583eb12327e3c5c1642a404a9cacf94e34f5"
@@ -5757,6 +6107,36 @@ mississippi@^1.3.0:
     stream-each "^1.1.0"
     through2 "^2.0.0"
 
+mississippi@^2.0.0:
+  version "2.0.0"
+  resolved "https://registry.yarnpkg.com/mississippi/-/mississippi-2.0.0.tgz#3442a508fafc28500486feea99409676e4ee5a6f"
+  dependencies:
+    concat-stream "^1.5.0"
+    duplexify "^3.4.2"
+    end-of-stream "^1.1.0"
+    flush-write-stream "^1.0.0"
+    from2 "^2.1.0"
+    parallel-transform "^1.1.0"
+    pump "^2.0.1"
+    pumpify "^1.3.3"
+    stream-each "^1.1.0"
+    through2 "^2.0.0"
+
+mississippi@^3.0.0:
+  version "3.0.0"
+  resolved "https://registry.yarnpkg.com/mississippi/-/mississippi-3.0.0.tgz#ea0a3291f97e0b5e8776b363d5f0a12d94c67022"
+  dependencies:
+    concat-stream "^1.5.0"
+    duplexify "^3.4.2"
+    end-of-stream "^1.1.0"
+    flush-write-stream "^1.0.0"
+    from2 "^2.1.0"
+    parallel-transform "^1.1.0"
+    pump "^3.0.0"
+    pumpify "^1.3.3"
+    stream-each "^1.1.0"
+    through2 "^2.0.0"
+
 mixin-object@^2.0.1:
   version "2.0.1"
   resolved "https://registry.yarnpkg.com/mixin-object/-/mixin-object-2.0.1.tgz#4fb949441dab182540f1fe035ba60e1947a5e57e"
@@ -5826,6 +6206,10 @@ ms@2.0.0:
   version "2.0.0"
   resolved "https://registry.yarnpkg.com/ms/-/ms-2.0.0.tgz#5608aeadfc00be6c2901df5f9861788de0d597c8"
 
+ms@^2.0.0:
+  version "2.1.1"
+  resolved "https://registry.yarnpkg.com/ms/-/ms-2.1.1.tgz#30a5864eb3ebb0a66f2ebe6d727af06a09d86e0a"
+
 multi-glob@^1.0.1:
   version "1.0.1"
   resolved "https://registry.yarnpkg.com/multi-glob/-/multi-glob-1.0.1.tgz#e67d2ab4429d27606e6eb4db35094afc91788750"
@@ -5907,6 +6291,14 @@ node-alias@^1.0.4:
     chalk "^1.1.1"
     lodash "^4.2.0"
 
+node-fetch-npm@^2.0.2:
+  version "2.0.2"
+  resolved "https://registry.yarnpkg.com/node-fetch-npm/-/node-fetch-npm-2.0.2.tgz#7258c9046182dca345b4208eda918daf33697ff7"
+  dependencies:
+    encoding "^0.1.11"
+    json-parse-better-errors "^1.0.0"
+    safe-buffer "^5.1.1"
+
 node-fetch@^1.0.1:
   version "1.7.3"
   resolved "https://registry.yarnpkg.com/node-fetch/-/node-fetch-1.7.3.tgz#980f6f72d85211a5347c6b2bc18c5b84c3eb47ef"
@@ -5914,6 +6306,24 @@ node-fetch@^1.0.1:
     encoding "^0.1.11"
     is-stream "^1.0.1"
 
+node-gyp@^3.6.2:
+  version "3.6.2"
+  resolved "https://registry.yarnpkg.com/node-gyp/-/node-gyp-3.6.2.tgz#9bfbe54562286284838e750eac05295853fa1c60"
+  dependencies:
+    fstream "^1.0.0"
+    glob "^7.0.3"
+    graceful-fs "^4.1.2"
+    minimatch "^3.0.2"
+    mkdirp "^0.5.0"
+    nopt "2 || 3"
+    npmlog "0 || 1 || 2 || 3 || 4"
+    osenv "0"
+    request "2"
+    rimraf "2"
+    semver "~5.3.0"
+    tar "^2.0.0"
+    which "1"
+
 node-gyp@~3.4.0:
   version "3.4.0"
   resolved "https://registry.yarnpkg.com/node-gyp/-/node-gyp-3.4.0.tgz#dda558393b3ecbbe24c9e6b8703c71194c63fa36"
@@ -6004,7 +6414,7 @@ nomnom@1.8.1, "nomnom@>= 1.5.x":
   dependencies:
     abbrev "1"
 
-nopt@^4.0.1:
+nopt@^4.0.1, nopt@~4.0.1:
   version "4.0.1"
   resolved "https://registry.yarnpkg.com/nopt/-/nopt-4.0.1.tgz#d0d4685afd5415193c8c7505602d0d17cd64474d"
   dependencies:
@@ -6015,7 +6425,7 @@ normalize-git-url@~3.0.2:
   version "3.0.2"
   resolved "https://registry.yarnpkg.com/normalize-git-url/-/normalize-git-url-3.0.2.tgz#8e5f14be0bdaedb73e07200310aa416c27350fc4"
 
-normalize-package-data@^2.0.0, normalize-package-data@^2.3.2, "normalize-package-data@~1.0.1 || ^2.0.0":
+normalize-package-data@^2.0.0, normalize-package-data@^2.3.2, normalize-package-data@^2.4.0, "normalize-package-data@~1.0.1 || ^2.0.0", normalize-package-data@~2.4.0:
   version "2.4.0"
   resolved "https://registry.yarnpkg.com/normalize-package-data/-/normalize-package-data-2.4.0.tgz#12f95a307d58352075a04907b84ac8be98ac012f"
   dependencies:
@@ -6052,6 +6462,10 @@ normalize-url@^1.4.0:
     query-string "^4.1.0"
     sort-keys "^1.0.0"
 
+npm-bundled@^1.0.1:
+  version "1.0.3"
+  resolved "https://registry.yarnpkg.com/npm-bundled/-/npm-bundled-1.0.3.tgz#7e71703d973af3370a9591bafe3a63aca0be2308"
+
 npm-cache-filename@~1.0.2:
   version "1.0.2"
   resolved "https://registry.yarnpkg.com/npm-cache-filename/-/npm-cache-filename-1.0.2.tgz#ded306c5b0bfc870a9e9faf823bc5f283e05ae11"
@@ -6086,6 +6500,23 @@ npm-install-checks@~3.0.0:
   dependencies:
     semver "^2.3.0 || 3.x || 4 || 5"
 
+npm-lifecycle@^2.0.0, npm-lifecycle@^2.0.1:
+  version "2.0.1"
+  resolved "https://registry.yarnpkg.com/npm-lifecycle/-/npm-lifecycle-2.0.1.tgz#897313f05ed24db8e28d99fa8b42c31b625e6237"
+  dependencies:
+    byline "^5.0.0"
+    graceful-fs "^4.1.11"
+    node-gyp "^3.6.2"
+    resolve-from "^4.0.0"
+    slide "^1.1.6"
+    uid-number "0.0.6"
+    umask "^1.1.0"
+    which "^1.3.0"
+
+npm-logical-tree@^1.2.1:
+  version "1.2.1"
+  resolved "https://registry.yarnpkg.com/npm-logical-tree/-/npm-logical-tree-1.2.1.tgz#44610141ca24664cad35d1e607176193fd8f5b88"
+
 "npm-package-arg@^3.0.0 || ^4.0.0", npm-package-arg@^4.1.1, npm-package-arg@~4.2.0:
   version "4.2.1"
   resolved "https://registry.yarnpkg.com/npm-package-arg/-/npm-package-arg-4.2.1.tgz#593303fdea85f7c422775f17f9eb7670f680e3ec"
@@ -6093,7 +6524,16 @@ npm-install-checks@~3.0.0:
     hosted-git-info "^2.1.5"
     semver "^5.1.0"
 
-"npm-package-arg@^4.0.0 || ^5.0.0":
+"npm-package-arg@^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0", "npm-package-arg@^4.0.0 || ^5.0.0 || ^6.0.0", npm-package-arg@^6.0.0:
+  version "6.1.0"
+  resolved "https://registry.yarnpkg.com/npm-package-arg/-/npm-package-arg-6.1.0.tgz#15ae1e2758a5027efb4c250554b85a737db7fcc1"
+  dependencies:
+    hosted-git-info "^2.6.0"
+    osenv "^0.1.5"
+    semver "^5.5.0"
+    validate-npm-package-name "^3.0.0"
+
+"npm-package-arg@^4.0.0 || ^5.0.0", npm-package-arg@^5.1.2:
   version "5.1.2"
   resolved "https://registry.yarnpkg.com/npm-package-arg/-/npm-package-arg-5.1.2.tgz#fb18d17bb61e60900d6312619919bd753755ab37"
   dependencies:
@@ -6102,6 +6542,54 @@ npm-install-checks@~3.0.0:
     semver "^5.1.0"
     validate-npm-package-name "^3.0.0"
 
+npm-package-arg@~6.0.0:
+  version "6.0.0"
+  resolved "https://registry.yarnpkg.com/npm-package-arg/-/npm-package-arg-6.0.0.tgz#8cce04b49d3f9faec3f56b0fe5f4391aeb9d2fac"
+  dependencies:
+    hosted-git-info "^2.5.0"
+    osenv "^0.1.4"
+    semver "^5.4.1"
+    validate-npm-package-name "^3.0.0"
+
+npm-packlist@^1.1.10, npm-packlist@~1.1.10:
+  version "1.1.10"
+  resolved "https://registry.yarnpkg.com/npm-packlist/-/npm-packlist-1.1.10.tgz#1039db9e985727e464df066f4cf0ab6ef85c398a"
+  dependencies:
+    ignore-walk "^3.0.1"
+    npm-bundled "^1.0.1"
+
+npm-pick-manifest@^2.1.0:
+  version "2.1.0"
+  resolved "https://registry.yarnpkg.com/npm-pick-manifest/-/npm-pick-manifest-2.1.0.tgz#dc381bdd670c35d81655e1d5a94aa3dd4d87fce5"
+  dependencies:
+    npm-package-arg "^6.0.0"
+    semver "^5.4.1"
+
+npm-profile@^3.0.1:
+  version "3.0.1"
+  resolved "https://registry.yarnpkg.com/npm-profile/-/npm-profile-3.0.1.tgz#65a1018340f14399a086b5d0a9bd0d13145d8e57"
+  dependencies:
+    aproba "^1.1.2"
+    make-fetch-happen "^2.5.0"
+
+npm-registry-client@^8.5.1:
+  version "8.5.1"
+  resolved "https://registry.yarnpkg.com/npm-registry-client/-/npm-registry-client-8.5.1.tgz#8115809c0a4b40938b8a109b8ea74d26c6f5d7f1"
+  dependencies:
+    concat-stream "^1.5.2"
+    graceful-fs "^4.1.6"
+    normalize-package-data "~1.0.1 || ^2.0.0"
+    npm-package-arg "^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0"
+    once "^1.3.3"
+    request "^2.74.0"
+    retry "^0.10.0"
+    safe-buffer "^5.1.1"
+    semver "2 >=2.2.1 || 3.x || 4 || 5"
+    slide "^1.1.3"
+    ssri "^5.2.4"
+  optionalDependencies:
+    npmlog "2 || ^3.1.0 || ^4.0.0"
+
 npm-registry-client@~7.2.1:
   version "7.2.1"
   resolved "https://registry.yarnpkg.com/npm-registry-client/-/npm-registry-client-7.2.1.tgz#c792266b088cc313f8525e7e35248626c723db75"
@@ -6128,6 +6616,10 @@ npm-user-validate@~0.1.5:
   version "0.1.5"
   resolved "https://registry.yarnpkg.com/npm-user-validate/-/npm-user-validate-0.1.5.tgz#52465d50c2d20294a57125b996baedbf56c5004b"
 
+npm-user-validate@~1.0.0:
+  version "1.0.0"
+  resolved "https://registry.yarnpkg.com/npm-user-validate/-/npm-user-validate-1.0.0.tgz#8ceca0f5cea04d4e93519ef72d0557a75122e951"
+
 npm@^3, npm@^3.10.6:
   version "3.10.10"
   resolved "https://registry.yarnpkg.com/npm/-/npm-3.10.10.tgz#5b1d577e4c8869d6c8603bc89e9cd1637303e46e"
@@ -6204,6 +6696,110 @@ npm@^3, npm@^3.10.6:
     wrappy "~1.0.2"
     write-file-atomic "~1.2.0"
 
+npm@^5.7.1:
+  version "5.8.0"
+  resolved "https://registry.yarnpkg.com/npm/-/npm-5.8.0.tgz#5e4bfb8c2e7ada01dd41ec0555d13dd0f446ddb2"
+  dependencies:
+    JSONStream "^1.3.2"
+    abbrev "~1.1.1"
+    ansi-regex "~3.0.0"
+    ansicolors "~0.3.2"
+    ansistyles "~0.1.3"
+    aproba "~1.2.0"
+    archy "~1.0.0"
+    bin-links "^1.1.0"
+    bluebird "~3.5.1"
+    cacache "^10.0.4"
+    call-limit "~1.1.0"
+    chownr "~1.0.1"
+    cli-table2 "~0.2.0"
+    cmd-shim "~2.0.2"
+    columnify "~1.5.4"
+    config-chain "~1.1.11"
+    detect-indent "~5.0.0"
+    detect-newline "^2.1.0"
+    dezalgo "~1.0.3"
+    editor "~1.0.0"
+    find-npm-prefix "^1.0.2"
+    fs-vacuum "~1.2.10"
+    fs-write-stream-atomic "~1.0.10"
+    gentle-fs "^2.0.1"
+    glob "~7.1.2"
+    graceful-fs "~4.1.11"
+    has-unicode "~2.0.1"
+    hosted-git-info "^2.6.0"
+    iferr "~0.1.5"
+    inflight "~1.0.6"
+    inherits "~2.0.3"
+    ini "^1.3.5"
+    init-package-json "^1.10.3"
+    is-cidr "~1.0.0"
+    json-parse-better-errors "^1.0.1"
+    lazy-property "~1.0.0"
+    libcipm "^1.6.0"
+    libnpx "^10.0.1"
+    lockfile "~1.0.3"
+    lodash._baseuniq "~4.6.0"
+    lodash.clonedeep "~4.5.0"
+    lodash.union "~4.6.0"
+    lodash.uniq "~4.5.0"
+    lodash.without "~4.4.0"
+    lru-cache "~4.1.1"
+    meant "~1.0.1"
+    mississippi "^3.0.0"
+    mkdirp "~0.5.1"
+    move-concurrently "^1.0.1"
+    nopt "~4.0.1"
+    normalize-package-data "~2.4.0"
+    npm-cache-filename "~1.0.2"
+    npm-install-checks "~3.0.0"
+    npm-lifecycle "^2.0.1"
+    npm-package-arg "~6.0.0"
+    npm-packlist "~1.1.10"
+    npm-profile "^3.0.1"
+    npm-registry-client "^8.5.1"
+    npm-user-validate "~1.0.0"
+    npmlog "~4.1.2"
+    once "~1.4.0"
+    opener "~1.4.3"
+    osenv "^0.1.5"
+    pacote "^7.6.1"
+    path-is-inside "~1.0.2"
+    promise-inflight "~1.0.1"
+    qrcode-terminal "~0.11.0"
+    query-string "^5.1.0"
+    qw "~1.0.1"
+    read "~1.0.7"
+    read-cmd-shim "~1.0.1"
+    read-installed "~4.0.3"
+    read-package-json "^2.0.13"
+    read-package-tree "~5.1.6"
+    readable-stream "^2.3.5"
+    request "~2.83.0"
+    retry "~0.10.1"
+    rimraf "~2.6.2"
+    safe-buffer "~5.1.1"
+    semver "^5.5.0"
+    sha "~2.0.1"
+    slide "~1.1.6"
+    sorted-object "~2.0.1"
+    sorted-union-stream "~2.1.3"
+    ssri "^5.2.4"
+    strip-ansi "~4.0.0"
+    tar "^4.4.0"
+    text-table "~0.2.0"
+    uid-number "0.0.6"
+    umask "~1.1.0"
+    unique-filename "~1.1.0"
+    unpipe "~1.0.0"
+    update-notifier "~2.3.0"
+    uuid "^3.2.1"
+    validate-npm-package-name "~3.0.0"
+    which "~1.3.0"
+    worker-farm "^1.5.4"
+    wrappy "~1.0.2"
+    write-file-atomic "^2.3.0"
+
 npmi@^2.0.1:
   version "2.0.1"
   resolved "https://registry.yarnpkg.com/npmi/-/npmi-2.0.1.tgz#32607657e1bd47ca857ab4e9d98f0a0cff96bcea"
@@ -6220,7 +6816,7 @@ npmi@^2.0.1:
     gauge "~2.6.0"
     set-blocking "~2.0.0"
 
-npmlog@^4.0.2:
+"npmlog@0 || 1 || 2 || 3 || 4", "npmlog@2 || ^3.1.0 || ^4.0.0", npmlog@^4.0.2, npmlog@~4.1.2:
   version "4.1.2"
   resolved "https://registry.yarnpkg.com/npmlog/-/npmlog-4.1.2.tgz#08a7f2a8bf734604779a9efa4ad5cc717abb954b"
   dependencies:
@@ -6357,7 +6953,7 @@ open@^0.0.5:
   version "0.0.5"
   resolved "https://registry.yarnpkg.com/open/-/open-0.0.5.tgz#42c3e18ec95466b6bf0dc42f3a2945c3f0cad8fc"
 
-opener@~1.4.2:
+opener@~1.4.2, opener@~1.4.3:
   version "1.4.3"
   resolved "https://registry.yarnpkg.com/opener/-/opener-1.4.3.tgz#5c6da2c5d7e5831e8ffa3964950f8d6674ac90b8"
 
@@ -6418,7 +7014,7 @@ os-tmpdir@^1.0.0, os-tmpdir@^1.0.1, os-tmpdir@~1.0.2:
   version "1.0.2"
   resolved "https://registry.yarnpkg.com/os-tmpdir/-/os-tmpdir-1.0.2.tgz#bbe67406c79aa85c5cfec766fe5734555dfa1274"
 
-osenv@0, osenv@^0.1.0, osenv@~0.1.3:
+osenv@0, osenv@^0.1.0, osenv@^0.1.5, osenv@~0.1.3:
   version "0.1.5"
   resolved "https://registry.yarnpkg.com/osenv/-/osenv-0.1.5.tgz#85cdfafaeb28e8677f416e287592b5f3f49ea410"
   dependencies:
@@ -6491,6 +7087,35 @@ package-json@^4.0.0:
     registry-url "^3.0.3"
     semver "^5.1.0"
 
+pacote@^7.5.1, pacote@^7.6.1:
+  version "7.6.1"
+  resolved "https://registry.yarnpkg.com/pacote/-/pacote-7.6.1.tgz#d44621c89a5a61f173989b60236757728387c094"
+  dependencies:
+    bluebird "^3.5.1"
+    cacache "^10.0.4"
+    get-stream "^3.0.0"
+    glob "^7.1.2"
+    lru-cache "^4.1.1"
+    make-fetch-happen "^2.6.0"
+    minimatch "^3.0.4"
+    mississippi "^3.0.0"
+    mkdirp "^0.5.1"
+    normalize-package-data "^2.4.0"
+    npm-package-arg "^6.0.0"
+    npm-packlist "^1.1.10"
+    npm-pick-manifest "^2.1.0"
+    osenv "^0.1.5"
+    promise-inflight "^1.0.1"
+    promise-retry "^1.1.1"
+    protoduck "^5.0.0"
+    rimraf "^2.6.2"
+    safe-buffer "^5.1.1"
+    semver "^5.5.0"
+    ssri "^5.2.4"
+    tar "^4.4.0"
+    unique-filename "^1.1.0"
+    which "^1.3.0"
+
 pako@~0.2.0:
   version "0.2.9"
   resolved "https://registry.yarnpkg.com/pako/-/pako-0.2.9.tgz#f3f7522f4ef782348da8161bad9ecfd51bf83a75"
@@ -6941,6 +7566,10 @@ preserve@^0.2.0:
   version "0.2.0"
   resolved "https://registry.yarnpkg.com/preserve/-/preserve-0.2.0.tgz#815ed1f6ebc65926f865b310c0713bcb3315ce4b"
 
+prettier@^1.12.1:
+  version "1.12.1"
+  resolved "https://registry.yarnpkg.com/prettier/-/prettier-1.12.1.tgz#c1ad20e803e7749faf905a409d2367e06bbe7325"
+
 private@^0.1.6, private@^0.1.7:
   version "0.1.7"
   resolved "https://registry.yarnpkg.com/private/-/private-0.1.7.tgz#68ce5e8a1ef0a23bb570cc28537b5332aba63ef1"
@@ -6949,6 +7578,10 @@ process-nextick-args@~1.0.6:
   version "1.0.7"
   resolved "https://registry.yarnpkg.com/process-nextick-args/-/process-nextick-args-1.0.7.tgz#150e20b756590ad3f91093f25a4f2ad8bff30ba3"
 
+process-nextick-args@~2.0.0:
+  version "2.0.0"
+  resolved "https://registry.yarnpkg.com/process-nextick-args/-/process-nextick-args-2.0.0.tgz#a37d732f4271b4ab1ad070d35508e8290788ffaa"
+
 process@^0.11.0:
   version "0.11.10"
   resolved "https://registry.yarnpkg.com/process/-/process-0.11.10.tgz#7332300e840161bda3e69a1d1d91a7d4bc16f182"
@@ -6957,10 +7590,17 @@ progress@^2.0.0:
   version "2.0.0"
   resolved "https://registry.yarnpkg.com/progress/-/progress-2.0.0.tgz#8a1be366bf8fc23db2bd23f10c6fe920b4389d1f"
 
-promise-inflight@^1.0.1:
+promise-inflight@^1.0.1, promise-inflight@~1.0.1:
   version "1.0.1"
   resolved "https://registry.yarnpkg.com/promise-inflight/-/promise-inflight-1.0.1.tgz#98472870bf228132fcbdd868129bad12c3c029e3"
 
+promise-retry@^1.1.1:
+  version "1.1.1"
+  resolved "https://registry.yarnpkg.com/promise-retry/-/promise-retry-1.1.1.tgz#6739e968e3051da20ce6497fb2b50f6911df3d6d"
+  dependencies:
+    err-code "^1.0.0"
+    retry "^0.10.0"
+
 "promise@>=3.2 <8", promise@^7.1.1:
   version "7.3.1"
   resolved "https://registry.yarnpkg.com/promise/-/promise-7.3.1.tgz#064b72602b18f90f29192b8b1bc418ffd1ebd3bf"
@@ -7002,6 +7642,12 @@ protocol-buffers-schema@^2.0.2:
   version "2.2.0"
   resolved "https://registry.yarnpkg.com/protocol-buffers-schema/-/protocol-buffers-schema-2.2.0.tgz#d29c6cd73fb655978fb6989691180db844119f61"
 
+protoduck@^5.0.0:
+  version "5.0.0"
+  resolved "https://registry.yarnpkg.com/protoduck/-/protoduck-5.0.0.tgz#752145e6be0ad834cb25716f670a713c860dce70"
+  dependencies:
+    genfun "^4.0.1"
+
 proxy-from-env@^1.0.0:
   version "1.0.0"
   resolved "https://registry.yarnpkg.com/proxy-from-env/-/proxy-from-env-1.0.0.tgz#33c50398f70ea7eb96d21f7b817630a55791c7ee"
@@ -7035,13 +7681,20 @@ pump@^1.0.0:
     end-of-stream "^1.1.0"
     once "^1.3.1"
 
-pump@^2.0.0:
+pump@^2.0.0, pump@^2.0.1:
   version "2.0.1"
   resolved "https://registry.yarnpkg.com/pump/-/pump-2.0.1.tgz#12399add6e4cf7526d973cbc8b5ce2e2908b3909"
   dependencies:
     end-of-stream "^1.1.0"
     once "^1.3.1"
 
+pump@^3.0.0:
+  version "3.0.0"
+  resolved "https://registry.yarnpkg.com/pump/-/pump-3.0.0.tgz#b4a2116815bde2f4e1ea602354e8c75565107a64"
+  dependencies:
+    end-of-stream "^1.1.0"
+    once "^1.3.1"
+
 pumpify@^1.3.3:
   version "1.4.0"
   resolved "https://registry.yarnpkg.com/pumpify/-/pumpify-1.4.0.tgz#80b7c5df7e24153d03f0e7ac8a05a5d068bd07fb"
@@ -7062,6 +7715,10 @@ q@^1.1.2:
   version "1.5.0"
   resolved "https://registry.yarnpkg.com/q/-/q-1.5.0.tgz#dd01bac9d06d30e6f219aecb8253ee9ebdc308f1"
 
+qrcode-terminal@~0.11.0:
+  version "0.11.0"
+  resolved "https://registry.yarnpkg.com/qrcode-terminal/-/qrcode-terminal-0.11.0.tgz#ffc6c28a2fc0bfb47052b47e23f4f446a5fbdb9e"
+
 qs@~0.6.0:
   version "0.6.6"
   resolved "https://registry.yarnpkg.com/qs/-/qs-0.6.6.tgz#6e015098ff51968b8a3c819001d5f2c89bc4b107"
@@ -7085,6 +7742,14 @@ query-string@^4.1.0, query-string@^4.2.2:
     object-assign "^4.1.0"
     strict-uri-encode "^1.0.0"
 
+query-string@^5.1.0:
+  version "5.1.1"
+  resolved "https://registry.yarnpkg.com/query-string/-/query-string-5.1.1.tgz#a78c012b71c17e05f2e3fa2319dd330682efb3cb"
+  dependencies:
+    decode-uri-component "^0.2.0"
+    object-assign "^4.1.0"
+    strict-uri-encode "^1.0.0"
+
 querystring-es3@^0.2.0:
   version "0.2.1"
   resolved "https://registry.yarnpkg.com/querystring-es3/-/querystring-es3-0.2.1.tgz#9ec61f79049875707d69414596fd907a4d711e73"
@@ -7112,6 +7777,10 @@ quote-stream@~0.0.0:
     minimist "0.0.8"
     through2 "~0.4.1"
 
+qw@~1.0.1:
+  version "1.0.1"
+  resolved "https://registry.yarnpkg.com/qw/-/qw-1.0.1.tgz#efbfdc740f9ad054304426acb183412cc8b996d4"
+
 randomatic@^1.1.3:
   version "1.1.7"
   resolved "https://registry.yarnpkg.com/randomatic/-/randomatic-1.1.7.tgz#c7abe9cc8b87c0baa876b19fde83fd464797e38c"
@@ -7155,6 +7824,10 @@ rc@^1.1.7:
     minimist "^1.2.0"
     strip-json-comments "~2.0.1"
 
+re-resizable@^4.3.1:
+  version "4.4.8"
+  resolved "https://registry.yarnpkg.com/re-resizable/-/re-resizable-4.4.8.tgz#1c7eedfd9b9ed1f83b3adfa7a97cda76881e4e57"
+
 react-ace@^5.0.1:
   version "5.2.2"
   resolved "https://registry.yarnpkg.com/react-ace/-/react-ace-5.2.2.tgz#2e35296531bcf3ba49f08ffb1ec482f8938a8d3b"
@@ -7202,15 +7875,6 @@ react-bootstrap-slider@2.0.1:
     react "^15.6.1"
     react-dom "^15.6.1"
 
-react-bootstrap-table@^4.0.2:
-  version "4.0.6"
-  resolved "https://registry.yarnpkg.com/react-bootstrap-table/-/react-bootstrap-table-4.0.6.tgz#23ab95e9363436abd1d13f4d67cc454a06a297e0"
-  dependencies:
-    classnames "^2.1.2"
-    prop-types "^15.5.10"
-    react-modal "^1.4.0"
-    react-s-alert "^1.3.0"
-
 react-bootstrap@^0.31.5:
   version "0.31.5"
   resolved "https://registry.yarnpkg.com/react-bootstrap/-/react-bootstrap-0.31.5.tgz#57040fa8b1274e1e074803c21a1b895fdabea05a"
@@ -7245,9 +7909,22 @@ react-datetime@2.9.0:
     prop-types "^15.5.7"
     react-onclickoutside "^5.9.0"
 
-react-dom-factories@^1.0.0:
-  version "1.0.2"
-  resolved "https://registry.yarnpkg.com/react-dom-factories/-/react-dom-factories-1.0.2.tgz#eb7705c4db36fb501b3aa38ff759616aa0ff96e0"
+react-dnd-html5-backend@^2.5.4:
+  version "2.6.0"
+  resolved "https://registry.yarnpkg.com/react-dnd-html5-backend/-/react-dnd-html5-backend-2.6.0.tgz#590cd1cca78441bb274edd571fef4c0b16ddcf8e"
+  dependencies:
+    lodash "^4.2.0"
+
+react-dnd@^2.5.4:
+  version "2.6.0"
+  resolved "https://registry.yarnpkg.com/react-dnd/-/react-dnd-2.6.0.tgz#7fa25676cf827d58a891293e3c1ab59da002545a"
+  dependencies:
+    disposables "^1.0.1"
+    dnd-core "^2.6.0"
+    hoist-non-react-statics "^2.1.0"
+    invariant "^2.1.0"
+    lodash "^4.2.0"
+    prop-types "^15.5.10"
 
 "react-dom@^15.0.0 || 15.x", react-dom@^15.6.1, react-dom@^15.6.2:
   version "15.6.2"
@@ -7258,13 +7935,6 @@ react-dom-factories@^1.0.0:
     object-assign "^4.1.0"
     prop-types "^15.5.10"
 
-react-draggable@3.x:
-  version "3.0.5"
-  resolved "https://registry.yarnpkg.com/react-draggable/-/react-draggable-3.0.5.tgz#c031e0ed4313531f9409d6cd84c8ebcec0ddfe2d"
-  dependencies:
-    classnames "^2.2.5"
-    prop-types "^15.6.0"
-
 "react-draggable@^2.2.6 || ^3.0.3":
   version "3.0.3"
   resolved "https://registry.yarnpkg.com/react-draggable/-/react-draggable-3.0.3.tgz#a6f9b3a7171981b76dadecf238316925cb9eacf4"
@@ -7280,16 +7950,6 @@ react-gravatar@^2.6.1:
     md5 "^2.1.0"
     query-string "^4.2.2"
 
-react-grid-layout@0.16.5:
-  version "0.16.5"
-  resolved "https://registry.yarnpkg.com/react-grid-layout/-/react-grid-layout-0.16.5.tgz#1ff12d12afa875c11fe05802f7509e52bfe9a2cb"
-  dependencies:
-    classnames "2.x"
-    lodash.isequal "^4.0.0"
-    prop-types "15.x"
-    react-draggable "3.x"
-    react-resizable "1.x"
-
 react-html-attributes@^1.3.0:
   version "1.4.1"
   resolved "https://registry.yarnpkg.com/react-html-attributes/-/react-html-attributes-1.4.1.tgz#97b5ec710da68833598c8be6f89ac436216840a5"
@@ -7321,17 +7981,6 @@ react-map-gl@^3.0.4:
     prop-types "^15.5.7"
     viewport-mercator-project "^4.0.1"
 
-react-modal@^1.4.0:
-  version "1.9.7"
-  resolved "https://registry.yarnpkg.com/react-modal/-/react-modal-1.9.7.tgz#07ef56790b953e3b98ef1e2989e347983c72871d"
-  dependencies:
-    create-react-class "^15.5.2"
-    element-class "^0.2.0"
-    exenv "1.2.0"
-    lodash.assign "^4.2.0"
-    prop-types "^15.5.7"
-    react-dom-factories "^1.0.0"
-
 react-onclickoutside@^5.9.0:
   version "5.11.1"
   resolved "https://registry.yarnpkg.com/react-onclickoutside/-/react-onclickoutside-5.11.1.tgz#00314e52567cf55faba94cabbacd119619070623"
@@ -7359,18 +8008,19 @@ react-redux@^5.0.2:
     loose-envify "^1.1.0"
     prop-types "^15.5.10"
 
-react-resizable@1.x, react-resizable@^1.3.3:
+react-resizable@^1.3.3:
   version "1.7.5"
   resolved "https://registry.yarnpkg.com/react-resizable/-/react-resizable-1.7.5.tgz#83eb75bb3684da6989bbbf4f826e1470f0af902e"
   dependencies:
     prop-types "15.x"
     react-draggable "^2.2.6 || ^3.0.3"
 
-react-s-alert@^1.3.0:
-  version "1.3.1"
-  resolved "https://registry.yarnpkg.com/react-s-alert/-/react-s-alert-1.3.1.tgz#4de6e8258cd233bcffef84f73fbf46ea9507dcc9"
+react-search-input@^0.11.3:
+  version "0.11.3"
+  resolved "https://registry.yarnpkg.com/react-search-input/-/react-search-input-0.11.3.tgz#3dd1f9fc584b6bc40a6ee133ae042b6fbb7ae8dd"
   dependencies:
-    babel-runtime "^6.23.0"
+    fuse.js "^3.0.0"
+    prop-types "^15.5.8"
 
 react-select-fast-filter-options@^0.2.1:
   version "0.2.3"
@@ -7511,7 +8161,7 @@ read-all-stream@^3.0.0:
     pinkie-promise "^2.0.0"
     readable-stream "^2.0.0"
 
-read-cmd-shim@~1.0.1:
+read-cmd-shim@^1.0.1, read-cmd-shim@~1.0.1:
   version "1.0.1"
   resolved "https://registry.yarnpkg.com/read-cmd-shim/-/read-cmd-shim-1.0.1.tgz#2d5d157786a37c055d22077c32c53f8329e91c7b"
   dependencies:
@@ -7530,7 +8180,7 @@ read-installed@~4.0.3:
   optionalDependencies:
     graceful-fs "^4.1.2"
 
-"read-package-json@1 || 2", read-package-json@^2.0.0, read-package-json@~2.0.4:
+"read-package-json@1 || 2", read-package-json@^2.0.0, read-package-json@^2.0.12, read-package-json@^2.0.13, read-package-json@~2.0.4:
   version "2.0.13"
   resolved "https://registry.yarnpkg.com/read-package-json/-/read-package-json-2.0.13.tgz#2e82ebd9f613baa6d2ebe3aa72cefe3f68e41f4a"
   dependencies:
@@ -7541,7 +8191,7 @@ read-installed@~4.0.3:
   optionalDependencies:
     graceful-fs "^4.1.2"
 
-read-package-tree@~5.1.5:
+read-package-tree@~5.1.5, read-package-tree@~5.1.6:
   version "5.1.6"
   resolved "https://registry.yarnpkg.com/read-package-tree/-/read-package-tree-5.1.6.tgz#4f03e83d0486856fb60d97c94882841c2a7b1b7a"
   dependencies:
@@ -7608,7 +8258,19 @@ read@1, read@~1.0.1, read@~1.0.5, read@~1.0.7:
     isarray "0.0.1"
     string_decoder "~0.10.x"
 
-readable-stream@~1.1.0, readable-stream@~1.1.9:
+readable-stream@^2.3.5:
+  version "2.3.6"
+  resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-2.3.6.tgz#b11c27d88b8ff1fbe070643cf94b0c79ae1b0aaf"
+  dependencies:
+    core-util-is "~1.0.0"
+    inherits "~2.0.3"
+    isarray "~1.0.0"
+    process-nextick-args "~2.0.0"
+    safe-buffer "~5.1.1"
+    string_decoder "~1.1.1"
+    util-deprecate "~1.0.1"
+
+readable-stream@~1.1.0, readable-stream@~1.1.10, readable-stream@~1.1.9:
   version "1.1.14"
   resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-1.1.14.tgz#7cf4c54ef648e3813084c636dd2079e166c081d9"
   dependencies:
@@ -7712,7 +8374,11 @@ redux-thunk@^2.1.0:
   version "2.2.0"
   resolved "https://registry.yarnpkg.com/redux-thunk/-/redux-thunk-2.2.0.tgz#e615a16e16b47a19a515766133d1e3e99b7852e5"
 
-redux@^3.5.2:
+redux-undo@^1.0.0-beta9-9-7:
+  version "1.0.0-beta9-9-7"
+  resolved "https://registry.yarnpkg.com/redux-undo/-/redux-undo-1.0.0-beta9-9-7.tgz#fe3baa1b271423d7ddbbfc3a82c71b029a2db8ba"
+
+redux@^3.5.2, redux@^3.7.1:
   version "3.7.2"
   resolved "https://registry.yarnpkg.com/redux/-/redux-3.7.2.tgz#06b73123215901d25d065be342eb026bc1c8537b"
   dependencies:
@@ -7868,7 +8534,7 @@ request@2.81.0:
     tunnel-agent "^0.6.0"
     uuid "^3.0.0"
 
-request@^2.72.0, request@^2.79.0:
+request@^2.72.0, request@^2.79.0, request@~2.83.0:
   version "2.83.0"
   resolved "https://registry.yarnpkg.com/request/-/request-2.83.0.tgz#ca0b65da02ed62935887808e6f510381034e3356"
   dependencies:
@@ -7957,10 +8623,18 @@ require-uncached@^1.0.3:
     caller-path "^0.1.0"
     resolve-from "^1.0.0"
 
+resize-observer-polyfill@1.5.0:
+  version "1.5.0"
+  resolved "https://registry.yarnpkg.com/resize-observer-polyfill/-/resize-observer-polyfill-1.5.0.tgz#660ff1d9712a2382baa2cad450a4716209f9ca69"
+
 resolve-from@^1.0.0:
   version "1.0.1"
   resolved "https://registry.yarnpkg.com/resolve-from/-/resolve-from-1.0.1.tgz#26cbfe935d1aeeeabb29bc3fe5aeb01e93d44226"
 
+resolve-from@^4.0.0:
+  version "4.0.0"
+  resolved "https://registry.yarnpkg.com/resolve-from/-/resolve-from-4.0.0.tgz#4abcd852ad32dd7baabfe9b40e00a36db5f392e6"
+
 resolve-protobuf-schema@^2.0.0:
   version "2.0.0"
   resolved "https://registry.yarnpkg.com/resolve-protobuf-schema/-/resolve-protobuf-schema-2.0.0.tgz#e67b062a67f02d11bd6886e70efda788407e0fb4"
@@ -7987,7 +8661,7 @@ restore-cursor@^2.0.0:
     onetime "^2.0.0"
     signal-exit "^3.0.2"
 
-retry@^0.10.0, retry@~0.10.0:
+retry@^0.10.0, retry@~0.10.0, retry@~0.10.1:
   version "0.10.1"
   resolved "https://registry.yarnpkg.com/retry/-/retry-0.10.1.tgz#e76388d217992c252750241d3d3956fed98d8ff4"
 
@@ -7997,7 +8671,7 @@ right-align@^0.1.1:
   dependencies:
     align-text "^0.1.1"
 
-rimraf@2, rimraf@^2.2.8, rimraf@^2.5.1, rimraf@^2.5.2, rimraf@^2.5.4, rimraf@^2.6.1:
+rimraf@2, rimraf@^2.2.8, rimraf@^2.5.1, rimraf@^2.5.2, rimraf@^2.5.4, rimraf@^2.6.1, rimraf@^2.6.2, rimraf@~2.6.2:
   version "2.6.2"
   resolved "https://registry.yarnpkg.com/rimraf/-/rimraf-2.6.2.tgz#2ed8150d24a16ea8651e6d6ef0f47c4158ce7a36"
   dependencies:
@@ -8101,7 +8775,7 @@ semver-utils@^1.1.1:
   version "1.1.1"
   resolved "https://registry.yarnpkg.com/semver-utils/-/semver-utils-1.1.1.tgz#27d92fec34d27cfa42707d3b40d025ae9855f2df"
 
-"semver@2 >=2.2.1 || 3.x || 4 || 5", "semver@2.x || 3.x || 4 || 5", "semver@^2.3.0 || 3.x || 4 || 5", semver@^5.0.1, semver@^5.0.3, semver@^5.1.0:
+"semver@2 >=2.2.1 || 3.x || 4 || 5", "semver@2.x || 3.x || 4 || 5", "semver@^2.3.0 || 3.x || 4 || 5", semver@^5.0.1, semver@^5.0.3, semver@^5.1.0, semver@^5.4.1, semver@^5.5.0:
   version "5.5.0"
   resolved "https://registry.yarnpkg.com/semver/-/semver-5.5.0.tgz#dc4bbc7a6ca9d916dee5d43516f0092b58f7b8ab"
 
@@ -8232,10 +8906,14 @@ slice-ansi@1.0.0:
   dependencies:
     is-fullwidth-code-point "^2.0.0"
 
-slide@^1.1.3, slide@^1.1.5, slide@~1.1.3, slide@~1.1.6:
+slide@^1.1.3, slide@^1.1.5, slide@^1.1.6, slide@~1.1.3, slide@~1.1.6:
   version "1.1.6"
   resolved "https://registry.yarnpkg.com/slide/-/slide-1.1.6.tgz#56eb027d65b4d2dce6cb2e2d32c4d4afc9e1d707"
 
+smart-buffer@^1.0.13:
+  version "1.1.15"
+  resolved "https://registry.yarnpkg.com/smart-buffer/-/smart-buffer-1.1.15.tgz#7f114b5b65fab3e2a35aa775bb12f0d1c649bf16"
+
 sntp@0.2.x:
   version "0.2.4"
   resolved "https://registry.yarnpkg.com/sntp/-/sntp-0.2.4.tgz#fb885f18b0f3aad189f824862536bceeec750900"
@@ -8407,6 +9085,20 @@ snyk@^1.25.1:
     url "^0.11.0"
     uuid "^3.0.1"
 
+socks-proxy-agent@^3.0.1:
+  version "3.0.1"
+  resolved "https://registry.yarnpkg.com/socks-proxy-agent/-/socks-proxy-agent-3.0.1.tgz#2eae7cf8e2a82d34565761539a7f9718c5617659"
+  dependencies:
+    agent-base "^4.1.0"
+    socks "^1.1.10"
+
+socks@^1.1.10:
+  version "1.1.10"
+  resolved "https://registry.yarnpkg.com/socks/-/socks-1.1.10.tgz#5b8b7fc7c8f341c53ed056e929b7bf4de8ba7b5a"
+  dependencies:
+    ip "^1.1.4"
+    smart-buffer "^1.0.13"
+
 sort-asc@^0.1.0:
   version "0.1.0"
   resolved "https://registry.yarnpkg.com/sort-asc/-/sort-asc-0.1.0.tgz#ab799df61fc73ea0956c79c4b531ed1e9e7727e9"
@@ -8432,6 +9124,13 @@ sorted-object@~2.0.1:
   version "2.0.1"
   resolved "https://registry.yarnpkg.com/sorted-object/-/sorted-object-2.0.1.tgz#7d631f4bd3a798a24af1dffcfbfe83337a5df5fc"
 
+sorted-union-stream@~2.1.3:
+  version "2.1.3"
+  resolved "https://registry.yarnpkg.com/sorted-union-stream/-/sorted-union-stream-2.1.3.tgz#c7794c7e077880052ff71a8d4a2dbb4a9a638ac7"
+  dependencies:
+    from2 "^1.3.0"
+    stream-iterate "^1.1.0"
+
 source-list-map@^2.0.0:
   version "2.0.0"
   resolved "https://registry.yarnpkg.com/source-list-map/-/source-list-map-2.0.0.tgz#aaa47403f7b245a92fbc97ea08f250d6087ed085"
@@ -8524,6 +9223,12 @@ ssri@^5.0.0:
   dependencies:
     safe-buffer "^5.1.0"
 
+ssri@^5.2.4:
+  version "5.3.0"
+  resolved "https://registry.yarnpkg.com/ssri/-/ssri-5.3.0.tgz#ba3872c9c6d33a0704a7d71ff045e5ec48999d06"
+  dependencies:
+    safe-buffer "^5.1.1"
+
 static-eval@~0.2.0:
   version "0.2.4"
   resolved "https://registry.yarnpkg.com/static-eval/-/static-eval-0.2.4.tgz#b7d34d838937b969f9641ca07d48f8ede263ea7b"
@@ -8570,6 +9275,13 @@ stream-http@^2.3.1:
     to-arraybuffer "^1.0.0"
     xtend "^4.0.0"
 
+stream-iterate@^1.1.0:
+  version "1.2.0"
+  resolved "https://registry.yarnpkg.com/stream-iterate/-/stream-iterate-1.2.0.tgz#2bd7c77296c1702a46488b8ad41f79865eecd4e1"
+  dependencies:
+    readable-stream "^2.1.5"
+    stream-shift "^1.0.0"
+
 stream-shift@^1.0.0:
   version "1.0.0"
   resolved "https://registry.yarnpkg.com/stream-shift/-/stream-shift-1.0.0.tgz#d5c752825e5367e786f78e18e445ea223a155952"
@@ -8623,6 +9335,12 @@ string_decoder@~1.0.3:
   dependencies:
     safe-buffer "~5.1.0"
 
+string_decoder@~1.1.1:
+  version "1.1.1"
+  resolved "https://registry.yarnpkg.com/string_decoder/-/string_decoder-1.1.1.tgz#9cf1611ba62685d7030ae9e4ba34149c3af03fc8"
+  dependencies:
+    safe-buffer "~5.1.0"
+
 stringstream@~0.0.4, stringstream@~0.0.5:
   version "0.0.5"
   resolved "https://registry.yarnpkg.com/stringstream/-/stringstream-0.0.5.tgz#4e484cd4de5a0bbbee18e46307710a8a81621878"
@@ -8633,7 +9351,7 @@ strip-ansi@^3.0.0, strip-ansi@^3.0.1, strip-ansi@~3.0.1:
   dependencies:
     ansi-regex "^2.0.0"
 
-strip-ansi@^4.0.0:
+strip-ansi@^4.0.0, strip-ansi@~4.0.0:
   version "4.0.0"
   resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-4.0.0.tgz#a8479022eb1ac368a871389b635262c505ee368f"
   dependencies:
@@ -8772,6 +9490,18 @@ tar@^2.0.0, tar@^2.2.1, tar@~2.2.1:
     fstream "^1.0.2"
     inherits "2"
 
+tar@^4.4.0:
+  version "4.4.1"
+  resolved "https://registry.yarnpkg.com/tar/-/tar-4.4.1.tgz#b25d5a8470c976fd7a9a8a350f42c59e9fa81749"
+  dependencies:
+    chownr "^1.0.1"
+    fs-minipass "^1.2.5"
+    minipass "^2.2.4"
+    minizlib "^1.1.0"
+    mkdirp "^0.5.0"
+    safe-buffer "^5.1.1"
+    yallist "^3.0.2"
+
 tempfile@^1.1.1:
   version "1.1.1"
   resolved "https://registry.yarnpkg.com/tempfile/-/tempfile-1.1.1.tgz#5bcc4eaecc4ab2c707d8bc11d99ccc9a2cb287f2"
@@ -8820,7 +9550,7 @@ through2@~0.6.3:
     readable-stream ">=1.0.33-1 <1.1.0-0"
     xtend ">=4.0.0 <4.1.0-0"
 
-through@2, through@^2.3.6, through@^2.3.7, through@^2.3.8, through@~2.3.4:
+through@2, "through@>=2.2.7 <3", through@^2.3.6, through@^2.3.7, through@^2.3.8, through@~2.3.4:
   version "2.3.8"
   resolved "https://registry.yarnpkg.com/through/-/through-2.3.8.tgz#0dd4c9ffaabc357960b1b724115d7e0e86a2e1f5"
 
@@ -9004,7 +9734,7 @@ uid-number@0.0.6, uid-number@^0.0.6:
   version "0.0.6"
   resolved "https://registry.yarnpkg.com/uid-number/-/uid-number-0.0.6.tgz#0ea10e8035e8eb5b8e4449f06da1c730663baa81"
 
-umask@~1.1.0:
+umask@^1.1.0, umask@~1.1.0:
   version "1.1.0"
   resolved "https://registry.yarnpkg.com/umask/-/umask-1.1.0.tgz#f29cebf01df517912bb58ff9c4e50fde8e33320d"
 
@@ -9127,7 +9857,7 @@ update-notifier@^0.6.0:
     latest-version "^2.0.0"
     semver-diff "^2.0.0"
 
-update-notifier@^2.2.0:
+update-notifier@^2.2.0, update-notifier@~2.3.0:
   version "2.3.0"
   resolved "https://registry.yarnpkg.com/update-notifier/-/update-notifier-2.3.0.tgz#4e8827a6bb915140ab093559d7014e3ebb837451"
   dependencies:
@@ -9141,6 +9871,21 @@ update-notifier@^2.2.0:
     semver-diff "^2.0.0"
     xdg-basedir "^3.0.0"
 
+update-notifier@^2.3.0:
+  version "2.5.0"
+  resolved "https://registry.yarnpkg.com/update-notifier/-/update-notifier-2.5.0.tgz#d0744593e13f161e406acb1d9408b72cad08aff6"
+  dependencies:
+    boxen "^1.2.1"
+    chalk "^2.0.1"
+    configstore "^3.0.0"
+    import-lazy "^2.1.0"
+    is-ci "^1.0.10"
+    is-installed-globally "^0.1.0"
+    is-npm "^1.0.0"
+    latest-version "^3.0.0"
+    semver-diff "^2.0.0"
+    xdg-basedir "^3.0.0"
+
 urijs@^1.18.10:
   version "1.19.0"
   resolved "https://registry.yarnpkg.com/urijs/-/urijs-1.19.0.tgz#d8aa284d0e7469703a6988ad045c4cbfdf08ada0"
@@ -9192,6 +9937,10 @@ uuid@^3.0.0, uuid@^3.0.1, uuid@^3.1.0:
   version "3.1.0"
   resolved "https://registry.yarnpkg.com/uuid/-/uuid-3.1.0.tgz#3dd3d3e790abc24d7b0d3a034ffababe28ebbc04"
 
+uuid@^3.2.1:
+  version "3.2.1"
+  resolved "https://registry.yarnpkg.com/uuid/-/uuid-3.2.1.tgz#12c528bb9d58d0b9265d9a2f6f0fe8be17ff1f14"
+
 v8flags@^2.1.1:
   version "2.1.1"
   resolved "https://registry.yarnpkg.com/v8flags/-/v8flags-2.1.1.tgz#aab1a1fa30d45f88dd321148875ac02c0b55e5b4"
@@ -9205,7 +9954,7 @@ validate-npm-package-license@^3.0.1:
     spdx-correct "~1.0.0"
     spdx-expression-parse "~1.0.0"
 
-validate-npm-package-name@^3.0.0:
+validate-npm-package-name@^3.0.0, validate-npm-package-name@~3.0.0:
   version "3.0.0"
   resolved "https://registry.yarnpkg.com/validate-npm-package-name/-/validate-npm-package-name-3.0.0.tgz#5fa912d81eb7d0c74afc140de7317f0ca7df437e"
   dependencies:
@@ -9405,7 +10154,7 @@ which-module@^2.0.0:
   version "2.0.0"
   resolved "https://registry.yarnpkg.com/which-module/-/which-module-2.0.0.tgz#d9ef07dce77b9902b8a3a8fa4b31c3e3f7e6e87a"
 
-which@1, which@^1.1.1, which@^1.2.9:
+which@1, which@^1.1.1, which@^1.2.9, which@^1.3.0, which@~1.3.0:
   version "1.3.0"
   resolved "https://registry.yarnpkg.com/which/-/which-1.3.0.tgz#ff04bdfc010ee547d780bec38e1ac1c2777d253a"
   dependencies:
@@ -9472,6 +10221,12 @@ worker-farm@^1.5.2:
     errno "^0.1.4"
     xtend "^4.0.1"
 
+worker-farm@^1.5.4:
+  version "1.6.0"
+  resolved "https://registry.yarnpkg.com/worker-farm/-/worker-farm-1.6.0.tgz#aecc405976fab5a95526180846f0dba288f3a4a0"
+  dependencies:
+    errno "~0.1.7"
+
 wrap-ansi@^2.0.0:
   version "2.1.0"
   resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-2.1.0.tgz#d8fc3d284dd05794fe84973caecdd1cf824fdd85"
@@ -9491,7 +10246,7 @@ write-file-atomic@^1.1.2:
     imurmurhash "^0.1.4"
     slide "^1.1.5"
 
-write-file-atomic@^2.0.0:
+write-file-atomic@^2.0.0, write-file-atomic@^2.3.0:
   version "2.3.0"
   resolved "https://registry.yarnpkg.com/write-file-atomic/-/write-file-atomic-2.3.0.tgz#1ff61575c2e2a4e8e510d6fa4e243cce183999ab"
   dependencies:
@@ -9559,10 +10314,18 @@ y18n@^3.2.1:
   version "3.2.1"
   resolved "https://registry.yarnpkg.com/y18n/-/y18n-3.2.1.tgz#6d15fba884c08679c0d77e88e7759e811e07fa41"
 
+y18n@^4.0.0:
+  version "4.0.0"
+  resolved "https://registry.yarnpkg.com/y18n/-/y18n-4.0.0.tgz#95ef94f85ecc81d007c264e190a120f0a3c8566b"
+
 yallist@^2.1.2:
   version "2.1.2"
   resolved "https://registry.yarnpkg.com/yallist/-/yallist-2.1.2.tgz#1c11f9218f076089a47dd512f93c6699a6a81d52"
 
+yallist@^3.0.0, yallist@^3.0.2:
+  version "3.0.2"
+  resolved "https://registry.yarnpkg.com/yallist/-/yallist-3.0.2.tgz#8452b4bb7e83c7c188d8041c1a837c773d6d8bb9"
+
 yargs-parser@^2.4.1:
   version "2.4.1"
   resolved "https://registry.yarnpkg.com/yargs-parser/-/yargs-parser-2.4.1.tgz#85568de3cf150ff49fa51825f03a8c880ddcc5c4"
@@ -9576,6 +10339,29 @@ yargs-parser@^7.0.0:
   dependencies:
     camelcase "^4.1.0"
 
+yargs-parser@^9.0.2:
+  version "9.0.2"
+  resolved "https://registry.yarnpkg.com/yargs-parser/-/yargs-parser-9.0.2.tgz#9ccf6a43460fe4ed40a9bb68f48d43b8a68cc077"
+  dependencies:
+    camelcase "^4.1.0"
+
+yargs@^11.0.0:
+  version "11.0.0"
+  resolved "https://registry.yarnpkg.com/yargs/-/yargs-11.0.0.tgz#c052931006c5eee74610e5fc0354bedfd08a201b"
+  dependencies:
+    cliui "^4.0.0"
+    decamelize "^1.1.1"
+    find-up "^2.1.0"
+    get-caller-file "^1.0.1"
+    os-locale "^2.0.0"
+    require-directory "^2.1.1"
+    require-main-filename "^1.0.1"
+    set-blocking "^2.0.0"
+    string-width "^2.0.0"
+    which-module "^2.0.0"
+    y18n "^3.2.1"
+    yargs-parser "^9.0.2"
+
 yargs@^4.3.2:
   version "4.8.1"
   resolved "https://registry.yarnpkg.com/yargs/-/yargs-4.8.1.tgz#c0c42924ca4aaa6b0e6da1739dfb216439f9ddc0"
diff --git a/superset/templates/superset/dashboard.html b/superset/templates/superset/dashboard.html
index 1a158d9..b6574bc 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" data-bootstrap="{{ bootstrap_data }}" />
 {% endblock %}
diff --git a/superset/views/core.py b/superset/views/core.py
index fc6012b..acedd77 100755
--- a/superset/views/core.py
+++ b/superset/views/core.py
@@ -1602,7 +1602,7 @@ class Superset(BaseSupersetView):
         dashboard.slices = current_slices
         dashboard.position_json = json.dumps(positions, indent=4, sort_keys=True)
         md = dashboard.params_dict
-        dashboard.css = data['css']
+        dashboard.css = data.get('css')
         dashboard.dashboard_title = data['dashboard_title']
 
         if 'filter_immune_slices' not in md:

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

Mime
View raw message