You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@superset.apache.org by cc...@apache.org on 2018/06/22 00:54:21 UTC

[incubator-superset] 06/26: Dashboard builder rebased + linted (#4849)

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

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

commit 5ff440b101ae2c3ee41e184fea2c594fb8d2bfec
Author: Grace Guo <gr...@airbnb.com>
AuthorDate: Thu Apr 19 15:27:23 2018 -0700

    Dashboard builder rebased + linted (#4849)
    
    * define dashboard redux state
    
    * update dashboard state reducer
    
    * dashboard layout converter + grid render
    
    * builder pane + slice adder
    
    * Dashboard header + slice header controls
    
    * fix linting
    
    * 2nd code review comments
---
 superset/assets/package.json                       |   1 +
 .../assets/spec/javascripts/chart/Chart_spec.jsx   |   2 +-
 .../spec/javascripts/dashboard/Dashboard_spec.jsx  |  96 ++++--
 .../assets/spec/javascripts/dashboard/fixtures.jsx |  10 +-
 .../spec/javascripts/dashboard/reducers_spec.js    |  23 +-
 superset/assets/src/chart/Chart.jsx                |  38 +--
 superset/assets/src/chart/ChartContainer.jsx       |   2 +-
 superset/assets/src/chart/chartAction.js           |  14 +-
 superset/assets/src/chart/chartReducer.js          |  35 +--
 superset/assets/src/dashboard/actions.js           | 127 --------
 .../assets/src/dashboard/actions/dashboardState.js | 166 ++++++++++
 .../assets/src/dashboard/actions/datasources.js    |  35 +++
 .../assets/src/dashboard/actions/sliceEntities.js  |  93 ++++++
 .../src/dashboard/components/ActionMenuItem.jsx    |  45 +++
 .../assets/src/dashboard/components/Controls.jsx   | 133 ++------
 .../assets/src/dashboard/components/Dashboard.jsx  | 301 +++++++-----------
 .../dashboard/components/DashboardContainer.jsx    |  52 +++-
 .../assets/src/dashboard/components/GridCell.jsx   |  49 +--
 .../assets/src/dashboard/components/GridLayout.jsx | 210 +++++--------
 .../assets/src/dashboard/components/Header.jsx     | 164 ++++++----
 .../dashboard/components/RefreshIntervalModal.jsx  |   7 +-
 .../assets/src/dashboard/components/SaveModal.jsx  |  34 +-
 .../assets/src/dashboard/components/SliceAdder.jsx | 341 ++++++++++-----------
 .../dashboard/components/SliceAdderContainer.jsx   |  25 ++
 .../src/dashboard/components/SliceHeader.jsx       | 123 ++------
 .../dashboard/components/SliceHeaderControls.jsx   | 106 +++++++
 superset/assets/src/dashboard/index.jsx            |  28 +-
 superset/assets/src/dashboard/reducers.js          | 214 -------------
 .../src/dashboard/reducers/dashboardState.js       | 128 ++++++++
 .../assets/src/dashboard/reducers/datasources.js   |  17 +
 .../src/dashboard/reducers/getInitialState.js      | 109 +++++++
 superset/assets/src/dashboard/reducers/index.js    |  22 ++
 .../assets/src/dashboard/reducers/sliceEntities.js |  62 ++++
 .../assets/src/dashboard/util/dashboardHelper.js   |   9 +
 .../src/dashboard/util/dashboardLayoutConverter.js | 322 +++++++++++++++++++
 .../src/dashboard/v2/actions/messageToasts.js      |   1 -
 .../v2/components/BuilderComponentPane.jsx         |  58 +++-
 .../dashboard/v2/components/DashboardBuilder.jsx   |  11 +-
 .../src/dashboard/v2/components/DashboardGrid.jsx  |   3 +-
 .../dashboard/v2/components/DashboardHeader.jsx    |   2 +-
 .../v2/components/dnd/dragDroppableConfig.js       |   1 +
 .../src/dashboard/v2/components/dnd/handleDrop.js  |   1 +
 .../gridComponents/{Chart.jsx => ChartHolder.jsx}  |  11 +-
 .../v2/components/gridComponents/Column.jsx        |  29 +-
 .../dashboard/v2/components/gridComponents/Row.jsx |  29 +-
 .../v2/components/gridComponents/index.js          |   6 +-
 .../dashboard/v2/containers/DashboardBuilder.jsx   |   6 +-
 .../dashboard/v2/containers/DashboardComponent.jsx |  13 +-
 .../src/dashboard/v2/containers/DashboardGrid.jsx  |   9 +-
 .../dashboard/v2/containers/DashboardHeader.jsx    |  43 ++-
 superset/assets/src/dashboard/v2/reducers/index.js |  11 +-
 .../dashboard/v2/stylesheets/builder-sidepane.less | 103 +++++++
 .../src/dashboard/v2/stylesheets/builder.less      |   3 +-
 .../dashboard/v2/stylesheets/components/chart.less |   3 +-
 superset/assets/src/dashboard/v2/util/constants.js |   2 +-
 .../src/dashboard/v2/util/newComponentFactory.js   |   3 +-
 .../src/dashboard/v2/util/newEntitiesFromDrop.js   |   3 +-
 .../assets/src/dashboard/v2/util/propShapes.jsx    |  50 ++-
 .../src/explore/components/ExploreChartHeader.jsx  |   6 +-
 .../src/explore/components/ExploreChartPanel.jsx   |   6 +-
 .../explore/components/ExploreViewContainer.jsx    |  16 +-
 superset/assets/src/explore/exploreUtils.js        |   2 +-
 superset/assets/src/explore/index.jsx              |   2 +-
 superset/assets/src/explore/reducers/index.js      |   5 +-
 superset/assets/src/modules/utils.js               |   1 -
 superset/assets/src/visualizations/table.css       |   9 +-
 .../stylesheets/{dashboard.css => dashboard.less}  | 116 +++----
 superset/assets/stylesheets/superset.less          |  16 +-
 superset/models/core.py                            |  13 +-
 superset/templates/superset/dashboard.html         |   1 +
 superset/views/core.py                             |  27 +-
 71 files changed, 2299 insertions(+), 1465 deletions(-)

diff --git a/superset/assets/package.json b/superset/assets/package.json
index 75f9504..d20dad7 100644
--- a/superset/assets/package.json
+++ b/superset/assets/package.json
@@ -102,6 +102,7 @@
     "react-map-gl": "^3.0.4",
     "react-redux": "^5.0.2",
     "react-resizable": "^1.3.3",
+    "react-search-input": "^0.11.3",
     "react-select": "1.2.1",
     "react-select-fast-filter-options": "^0.2.1",
     "react-sortable-hoc": "^0.8.3",
diff --git a/superset/assets/spec/javascripts/chart/Chart_spec.jsx b/superset/assets/spec/javascripts/chart/Chart_spec.jsx
index b766d9f..29a2941 100644
--- a/superset/assets/spec/javascripts/chart/Chart_spec.jsx
+++ b/superset/assets/spec/javascripts/chart/Chart_spec.jsx
@@ -20,7 +20,7 @@ describe('Chart', () => {
   };
   const mockedProps = {
     ...chart,
-    chartKey: 'slice_223',
+    id: 223,
     containerId: 'slice-container-223',
     datasource: {},
     formData: {},
diff --git a/superset/assets/spec/javascripts/dashboard/Dashboard_spec.jsx b/superset/assets/spec/javascripts/dashboard/Dashboard_spec.jsx
index c6e94d8..f4def13 100644
--- a/superset/assets/spec/javascripts/dashboard/Dashboard_spec.jsx
+++ b/superset/assets/spec/javascripts/dashboard/Dashboard_spec.jsx
@@ -1,61 +1,68 @@
+/* eslint camelcase: 0 */
 import React from 'react';
 import { shallow } from 'enzyme';
 import { describe, it } from 'mocha';
 import { expect } from 'chai';
 import sinon from 'sinon';
 
-import * as dashboardActions from '../../../src/dashboard/actions';
+import * as sliceActions from '../../../src/dashboard/actions/sliceEntities';
+import * as dashboardActions from '../../../src/dashboard/actions/dashboardState';
 import * as chartActions from '../../../src/chart/chartAction';
 import Dashboard from '../../../src/dashboard/components/Dashboard';
-import { defaultFilters, dashboard, charts } from './fixtures';
+import { defaultFilters, dashboardState, dashboardInfo, dashboardLayout,
+  charts, datasources, sliceEntities } from './fixtures';
 
 describe('Dashboard', () => {
   const mockedProps = {
-    actions: { ...chartActions, ...dashboardActions },
+    actions: { ...chartActions, ...dashboardActions, ...sliceActions },
     initMessages: [],
-    dashboard: dashboard.dashboard,
-    slices: charts,
-    filters: dashboard.filters,
-    datasources: dashboard.datasources,
-    refresh: false,
+    dashboardState,
+    dashboardInfo,
+    charts,
+    slices: sliceEntities.slices,
+    datasources,
+    layout: dashboardLayout.present,
     timeout: 60,
-    isStarred: false,
-    userId: dashboard.userId,
+    userId: dashboardInfo.userId,
   };
 
   it('should render', () => {
     const wrapper = shallow(<Dashboard {...mockedProps} />);
     expect(wrapper.find('#dashboard-container')).to.have.length(1);
-    expect(wrapper.instance().getAllSlices()).to.have.length(3);
+    expect(wrapper.instance().getAllCharts()).to.have.length(3);
   });
 
   it('should handle metadata default_filters', () => {
     const wrapper = shallow(<Dashboard {...mockedProps} />);
-    expect(wrapper.instance().props.filters).deep.equal(defaultFilters);
+    expect(wrapper.instance().props.dashboardState.filters).deep.equal(defaultFilters);
   });
 
   describe('getFormDataExtra', () => {
     let wrapper;
-    let selectedSlice;
+    let selectedChart;
     beforeEach(() => {
       wrapper = shallow(<Dashboard {...mockedProps} />);
-      selectedSlice = wrapper.instance().props.dashboard.slices[1];
+      selectedChart = charts[248];
     });
 
     it('should carry default_filters', () => {
-      const extraFilters = wrapper.instance().getFormDataExtra(selectedSlice).extra_filters;
+      const extraFilters = wrapper.instance().getFormDataExtra(selectedChart).extra_filters;
       expect(extraFilters[0]).to.deep.equal({ col: 'region', op: 'in', val: [] });
       expect(extraFilters[1]).to.deep.equal({ col: 'country_name', op: 'in', val: ['United States'] });
     });
 
     it('should carry updated filter', () => {
-      wrapper.setProps({
+      const newState = {
+        ...wrapper.props('dashboardState'),
         filters: {
           256: { region: [] },
           257: { country_name: ['France'] },
         },
+      };
+      wrapper.setProps({
+        dashboardState: newState,
       });
-      const extraFilters = wrapper.instance().getFormDataExtra(selectedSlice).extra_filters;
+      const extraFilters = wrapper.instance().getFormDataExtra(selectedChart).extra_filters;
       expect(extraFilters[1]).to.deep.equal({ col: 'country_name', op: 'in', val: ['France'] });
     });
   });
@@ -65,7 +72,7 @@ describe('Dashboard', () => {
     let spy;
     beforeEach(() => {
       wrapper = shallow(<Dashboard {...mockedProps} />);
-      spy = sinon.spy(wrapper.instance(), 'fetchSlices');
+      spy = sinon.spy(mockedProps.actions, 'runQuery');
     });
     afterEach(() => {
       spy.restore();
@@ -75,13 +82,13 @@ describe('Dashboard', () => {
       const filterKey = Object.keys(defaultFilters)[1];
       wrapper.instance().refreshExcept(filterKey);
       expect(spy.callCount).to.equal(1);
-      expect(spy.getCall(0).args[0].length).to.equal(1);
+      const slice_id = spy.getCall(0).args[0].slice_id;
+      expect(slice_id).to.equal(248);
     });
 
     it('should refresh all slices', () => {
       wrapper.instance().refreshExcept();
-      expect(spy.callCount).to.equal(1);
-      expect(spy.getCall(0).args[0].length).to.equal(3);
+      expect(spy.callCount).to.equal(3);
     });
   });
 
@@ -94,7 +101,7 @@ describe('Dashboard', () => {
       wrapper = shallow(<Dashboard {...mockedProps} />);
       prevProp = wrapper.instance().props;
       refreshExceptSpy = sinon.spy(wrapper.instance(), 'refreshExcept');
-      fetchSlicesStub = sinon.stub(wrapper.instance(), 'fetchSlices');
+      fetchSlicesStub = sinon.stub(mockedProps.actions, 'fetchCharts');
     });
     afterEach(() => {
       fetchSlicesStub.restore();
@@ -106,48 +113,63 @@ describe('Dashboard', () => {
         refreshExceptSpy.reset();
       });
       it('no change', () => {
-        wrapper.setProps({
-          refresh: true,
+        const newState = {
+          ...wrapper.props('dashboardState'),
           filters: {
             256: { region: [] },
             257: { country_name: ['United States'] },
           },
+        };
+        wrapper.setProps({
+          dashboardState: newState,
         });
         wrapper.instance().componentDidUpdate(prevProp);
         expect(refreshExceptSpy.callCount).to.equal(0);
       });
 
       it('remove filter', () => {
-        wrapper.setProps({
+        const newState = {
+          ...wrapper.props('dashboardState'),
           refresh: true,
           filters: {
             256: { region: [] },
           },
+        };
+        wrapper.setProps({
+          dashboardState: newState,
         });
         wrapper.instance().componentDidUpdate(prevProp);
         expect(refreshExceptSpy.callCount).to.equal(1);
       });
 
       it('change filter', () => {
-        wrapper.setProps({
+        const newState = {
+          ...wrapper.props('dashboardState'),
           refresh: true,
           filters: {
             256: { region: [] },
             257: { country_name: ['Canada'] },
           },
+        };
+        wrapper.setProps({
+          dashboardState: newState,
         });
         wrapper.instance().componentDidUpdate(prevProp);
         expect(refreshExceptSpy.callCount).to.equal(1);
       });
 
       it('add filter', () => {
-        wrapper.setProps({
+        const newState = {
+          ...wrapper.props('dashboardState'),
           refresh: true,
           filters: {
             256: { region: [] },
             257: { country_name: ['Canada'] },
             258: { another_filter: ['new'] },
           },
+        };
+        wrapper.setProps({
+          dashboardState: newState,
         });
         wrapper.instance().componentDidUpdate(prevProp);
         expect(refreshExceptSpy.callCount).to.equal(1);
@@ -155,28 +177,36 @@ describe('Dashboard', () => {
     });
 
     it('should refresh if refresh flag is true', () => {
-      wrapper.setProps({
+      const newState = {
+        ...wrapper.props('dashboardState'),
         refresh: true,
         filters: {
           256: { region: ['Asian'] },
         },
+      };
+      wrapper.setProps({
+        dashboardState: newState,
       });
       wrapper.instance().componentDidUpdate(prevProp);
-      const fetchArgs = fetchSlicesStub.lastCall.args[0];
-      expect(fetchArgs).to.have.length(2);
+      expect(refreshExceptSpy.callCount).to.equal(1);
+      expect(refreshExceptSpy.lastCall.args[0]).to.equal('256');
     });
 
     it('should not refresh filter_immune_slices', () => {
-      wrapper.setProps({
+      const newState = {
+        ...wrapper.props('dashboardState'),
         refresh: true,
         filters: {
           256: { region: [] },
           257: { country_name: ['Canada'] },
         },
+      };
+      wrapper.setProps({
+        dashboardState: newState,
       });
       wrapper.instance().componentDidUpdate(prevProp);
-      const fetchArgs = fetchSlicesStub.lastCall.args[0];
-      expect(fetchArgs).to.have.length(1);
+      expect(refreshExceptSpy.callCount).to.equal(1);
+      expect(refreshExceptSpy.lastCall.args[0]).to.equal('257');
     });
   });
 });
diff --git a/superset/assets/spec/javascripts/dashboard/fixtures.jsx b/superset/assets/spec/javascripts/dashboard/fixtures.jsx
index 371b02c..1565ccd 100644
--- a/superset/assets/spec/javascripts/dashboard/fixtures.jsx
+++ b/superset/assets/spec/javascripts/dashboard/fixtures.jsx
@@ -1,4 +1,4 @@
-import { getInitialState } from '../../../src/dashboard/reducers';
+import getInitialState from '../../../src/dashboard/reducers/getInitialState';
 
 export const defaultFilters = {
   256: { region: [] },
@@ -118,7 +118,6 @@ export const slice = {
   slice_url: '/superset/explore/table/2/?form_data=%7B%22slice_id%22%3A%20248%7D',
 };
 
-const datasources = {};
 const mockDashboardData = {
   css: '',
   dash_edit_perm: true,
@@ -152,10 +151,13 @@ const mockDashboardData = {
   slices: [regionFilter, slice, countryFilter],
   standalone_mode: false,
 };
-export const { dashboard, charts } = getInitialState({
+export const {
+  dashboardState, dashboardInfo,
+  charts, datasources, sliceEntities,
+  dashboardLayout } = getInitialState({
   common: {},
   dashboard_data: mockDashboardData,
-  datasources,
+  datasources: {},
   user_id: '1',
 });
 
diff --git a/superset/assets/spec/javascripts/dashboard/reducers_spec.js b/superset/assets/spec/javascripts/dashboard/reducers_spec.js
index 6421fec..580a574 100644
--- a/superset/assets/spec/javascripts/dashboard/reducers_spec.js
+++ b/superset/assets/spec/javascripts/dashboard/reducers_spec.js
@@ -1,20 +1,23 @@
 import { describe, it } from 'mocha';
 import { expect } from 'chai';
 
-import { dashboard as reducers } from '../../../src/dashboard/reducers';
-import * as actions from '../../../src/dashboard/actions';
-import { defaultFilters, dashboard as initState } from './fixtures';
+import reducers from '../../../src/dashboard/reducers/dashboardState';
+import * as actions from '../../../src/dashboard/actions/dashboardState';
+import { defaultFilters, dashboardState as initState } from './fixtures';
 
 describe('Dashboard reducers', () => {
+  it('should initialized', () => {
+    expect(initState.sliceIds.size).to.equal(3);
+  });
+
   it('should remove slice', () => {
     const action = {
       type: actions.REMOVE_SLICE,
-      slice: initState.dashboard.slices[1],
+      sliceId: 248,
     };
-    expect(initState.dashboard.slices).to.have.length(3);
 
-    const { dashboard, filters, refresh } = reducers(initState, action);
-    expect(dashboard.slices).to.have.length(2);
+    const { sliceIds, filters, refresh } = reducers(initState, action);
+    expect(sliceIds.size).to.be.equal(2);
     expect(filters).to.deep.equal(defaultFilters);
     expect(refresh).to.equal(false);
   });
@@ -22,13 +25,13 @@ describe('Dashboard reducers', () => {
   it('should remove filter slice', () => {
     const action = {
       type: actions.REMOVE_SLICE,
-      slice: initState.dashboard.slices[0],
+      sliceId: 256,
     };
     const initFilters = Object.keys(initState.filters);
     expect(initFilters).to.have.length(2);
 
-    const { dashboard, filters, refresh } = reducers(initState, action);
-    expect(dashboard.slices).to.have.length(2);
+    const { sliceIds, filters, refresh } = reducers(initState, action);
+    expect(sliceIds.size).to.equal(2);
     expect(Object.keys(filters)).to.have.length(1);
     expect(refresh).to.equal(true);
   });
diff --git a/superset/assets/src/chart/Chart.jsx b/superset/assets/src/chart/Chart.jsx
index f223174..e9f7c63 100644
--- a/superset/assets/src/chart/Chart.jsx
+++ b/superset/assets/src/chart/Chart.jsx
@@ -17,7 +17,7 @@ import './chart.css';
 const propTypes = {
   annotationData: PropTypes.object,
   actions: PropTypes.object,
-  chartKey: PropTypes.string.isRequired,
+  chartId: PropTypes.number.isRequired,
   containerId: PropTypes.string.isRequired,
   datasource: PropTypes.object.isRequired,
   formData: PropTypes.object.isRequired,
@@ -42,7 +42,6 @@ const propTypes = {
   // dashboard callbacks
   addFilter: PropTypes.func,
   getFilters: PropTypes.func,
-  clearFilter: PropTypes.func,
   removeFilter: PropTypes.func,
   onQuery: PropTypes.func,
   onDismissRefreshOverlay: PropTypes.func,
@@ -51,7 +50,6 @@ const propTypes = {
 const defaultProps = {
   addFilter: () => ({}),
   getFilters: () => ({}),
-  clearFilter: () => ({}),
   removeFilter: () => ({}),
 };
 
@@ -67,7 +65,6 @@ class Chart extends React.PureComponent {
     this.datasource = props.datasource;
     this.addFilter = this.addFilter.bind(this);
     this.getFilters = this.getFilters.bind(this);
-    this.clearFilter = this.clearFilter.bind(this);
     this.removeFilter = this.removeFilter.bind(this);
     this.headerHeight = this.headerHeight.bind(this);
     this.height = this.height.bind(this);
@@ -78,7 +75,7 @@ class Chart extends React.PureComponent {
     if (this.props.triggerQuery) {
       this.props.actions.runQuery(this.props.formData, false,
         this.props.timeout,
-        this.props.chartKey,
+        this.props.chartId,
       );
     }
   }
@@ -92,15 +89,14 @@ class Chart extends React.PureComponent {
   }
 
   componentDidUpdate(prevProps) {
-    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)
+    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)
     ) {
       this.renderViz();
     }
@@ -118,10 +114,6 @@ class Chart extends React.PureComponent {
     this.props.addFilter(col, vals, merge, refresh);
   }
 
-  clearFilter() {
-    this.props.clearFilter();
-  }
-
   removeFilter(col, vals, refresh = true) {
     this.props.removeFilter(col, vals, refresh);
   }
@@ -150,7 +142,7 @@ class Chart extends React.PureComponent {
   }
 
   error(e) {
-    this.props.actions.chartRenderingFailed(e, this.props.chartKey);
+    this.props.actions.chartRenderingFailed(e, this.props.chartId);
   }
 
   verboseMetricName(metric) {
@@ -198,21 +190,21 @@ class Chart extends React.PureComponent {
       // [re]rendering the visualization
       viz(this, qr, this.props.setControlValue);
       Logger.append(LOG_ACTIONS_RENDER_EVENT, {
-        label: this.props.chartKey,
+        label: 'slice_' + this.props.chartId,
         vis_type: this.props.vizType,
         start_offset: renderStart,
         duration: Logger.getTimestamp() - renderStart,
       });
-      this.props.actions.chartRenderingSucceeded(this.props.chartKey);
+      this.props.actions.chartRenderingSucceeded(this.props.chartId);
     } catch (e) {
-      this.props.actions.chartRenderingFailed(e, this.props.chartKey);
+      this.props.actions.chartRenderingFailed(e, this.props.chartId);
     }
   }
 
   render() {
     const isLoading = this.props.chartStatus === 'loading';
     return (
-      <div className={`token col-md-12 ${isLoading ? 'is-loading' : ''}`}>
+      <div className={`${isLoading ? 'is-loading' : ''}`}>
         {this.renderTooltip()}
         {isLoading &&
           <Loading size={25} />
diff --git a/superset/assets/src/chart/ChartContainer.jsx b/superset/assets/src/chart/ChartContainer.jsx
index b731412..e3cb1f9 100644
--- a/superset/assets/src/chart/ChartContainer.jsx
+++ b/superset/assets/src/chart/ChartContainer.jsx
@@ -5,7 +5,7 @@ import * as Actions from './chartAction';
 import Chart from './Chart';
 
 function mapStateToProps({ charts }, ownProps) {
-  const chart = charts[ownProps.chartKey];
+  const chart = charts[ownProps.chartId];
   return {
     annotationData: chart.annotationData,
     chartAlert: chart.chartAlert,
diff --git a/superset/assets/src/chart/chartAction.js b/superset/assets/src/chart/chartAction.js
index cb24f65..33f49d1 100644
--- a/superset/assets/src/chart/chartAction.js
+++ b/superset/assets/src/chart/chartAction.js
@@ -117,6 +117,11 @@ export function updateQueryFormData(value, key) {
   return { type: UPDATE_QUERY_FORM_DATA, value, key };
 }
 
+export const ADD_CHART = 'ADD_CHART';
+export function addChart(chart, key) {
+  return { type: ADD_CHART, chart, key };
+}
+
 export const RUN_QUERY = 'RUN_QUERY';
 export function runQuery(formData, force = false, timeout = 60, key) {
   return (dispatch) => {
@@ -139,7 +144,7 @@ export function runQuery(formData, force = false, timeout = 60, key) {
       .then(() => queryRequest)
       .then((queryResponse) => {
         Logger.append(LOG_ACTIONS_LOAD_EVENT, {
-          label: key,
+          label: 'slice_' + key,
           is_cached: queryResponse.is_cached,
           row_count: queryResponse.rowcount,
           datasource: formData.datasource,
@@ -190,3 +195,10 @@ export function runQuery(formData, force = false, timeout = 60, key) {
     ]);
   };
 }
+
+export function refreshChart(chart, force, timeout) {
+  return dispatch => (
+    dispatch(runQuery(chart.latestQueryFormData, force, timeout, chart.id))
+  );
+}
+
diff --git a/superset/assets/src/chart/chartReducer.js b/superset/assets/src/chart/chartReducer.js
index f68a5b8..d57959a 100644
--- a/superset/assets/src/chart/chartReducer.js
+++ b/superset/assets/src/chart/chartReducer.js
@@ -1,25 +1,10 @@
 /* eslint camelcase: 0 */
-import PropTypes from 'prop-types';
-
 import { now } from '../modules/dates';
 import * as actions from './chartAction';
 import { t } from '../locales';
 
-export const chartPropType = {
-  chartKey: PropTypes.string.isRequired,
-  chartAlert: PropTypes.string,
-  chartStatus: PropTypes.string,
-  chartUpdateEndTime: PropTypes.number,
-  chartUpdateStartTime: PropTypes.number,
-  latestQueryFormData: PropTypes.object,
-  queryRequest: PropTypes.object,
-  queryResponse: PropTypes.object,
-  triggerQuery: PropTypes.bool,
-  lastRendered: PropTypes.number,
-};
-
 export const chart = {
-  chartKey: '',
+  id: 0,
   chartAlert: null,
   chartStatus: 'loading',
   chartUpdateEndTime: null,
@@ -33,6 +18,12 @@ export const chart = {
 
 export default function chartReducer(charts = {}, action) {
   const actionHandlers = {
+    [actions.ADD_CHART]() {
+      return {
+        ...chart,
+        ...action.chart,
+      };
+    },
     [actions.CHART_UPDATE_SUCCEEDED](state) {
       return { ...state,
         chartStatus: 'success',
@@ -70,12 +61,12 @@ export default function chartReducer(charts = {}, action) {
       return { ...state,
         chartStatus: 'failed',
         chartAlert: (
-            `${t('Query timeout')} - ` +
-            t(`visualization queries are set to timeout at ${action.timeout} seconds. `) +
-            t('Perhaps your data has grown, your database is under unusual load, ' +
-                'or you are simply querying a data source that is too large ' +
-                'to be processed within the timeout range. ' +
-                'If that is the case, we recommend that you summarize your data further.')),
+          `${t('Query timeout')} - ` +
+          t(`visualization queries are set to timeout at ${action.timeout} seconds. `) +
+          t('Perhaps your data has grown, your database is under unusual load, ' +
+            'or you are simply querying a data source that is too large ' +
+            'to be processed within the timeout range. ' +
+            'If that is the case, we recommend that you summarize your data further.')),
       };
     },
     [actions.CHART_UPDATE_FAILED](state) {
diff --git a/superset/assets/src/dashboard/actions.js b/superset/assets/src/dashboard/actions.js
deleted file mode 100644
index c7f1a6a..0000000
--- a/superset/assets/src/dashboard/actions.js
+++ /dev/null
@@ -1,127 +0,0 @@
-/* global notify */
-import $ from 'jquery';
-import { getExploreUrlAndPayload } from '../explore/exploreUtils';
-
-export const ADD_FILTER = 'ADD_FILTER';
-export function addFilter(sliceId, col, vals, merge = true, refresh = true) {
-  return { type: ADD_FILTER, sliceId, col, vals, merge, refresh };
-}
-
-export const CLEAR_FILTER = 'CLEAR_FILTER';
-export function clearFilter(sliceId) {
-  return { type: CLEAR_FILTER, sliceId };
-}
-
-export const REMOVE_FILTER = 'REMOVE_FILTER';
-export function removeFilter(sliceId, col, vals, refresh = true) {
-  return { type: REMOVE_FILTER, sliceId, col, vals, refresh };
-}
-
-export const UPDATE_DASHBOARD_LAYOUT = 'UPDATE_DASHBOARD_LAYOUT';
-export function updateDashboardLayout(layout) {
-  return { type: UPDATE_DASHBOARD_LAYOUT, layout };
-}
-
-export const UPDATE_DASHBOARD_TITLE = 'UPDATE_DASHBOARD_TITLE';
-export function updateDashboardTitle(title) {
-  return { type: UPDATE_DASHBOARD_TITLE, title };
-}
-
-export function addSlicesToDashboard(dashboardId, sliceIds) {
-  return () => (
-    $.ajax({
-      type: 'POST',
-      url: `/superset/add_slices/${dashboardId}/`,
-      data: {
-        data: JSON.stringify({ slice_ids: sliceIds }),
-      },
-    })
-      .done(() => {
-        // Refresh page to allow for slices to re-render
-        window.location.reload();
-      })
-  );
-}
-
-export const REMOVE_SLICE = 'REMOVE_SLICE';
-export function removeSlice(slice) {
-  return { type: REMOVE_SLICE, slice };
-}
-
-export const UPDATE_SLICE_NAME = 'UPDATE_SLICE_NAME';
-export function updateSliceName(slice, sliceName) {
-  return { type: UPDATE_SLICE_NAME, slice, sliceName };
-}
-export function saveSlice(slice, sliceName) {
-  const oldName = slice.slice_name;
-  return (dispatch) => {
-    const sliceParams = {};
-    sliceParams.slice_id = slice.slice_id;
-    sliceParams.action = 'overwrite';
-    sliceParams.slice_name = sliceName;
-
-    const { url, payload } = getExploreUrlAndPayload({
-      formData: slice.form_data,
-      endpointType: 'base',
-      force: false,
-      curUrl: null,
-      requestParams: sliceParams,
-    });
-    return $.ajax({
-      url,
-      type: 'POST',
-      data: {
-        form_data: JSON.stringify(payload),
-      },
-      success: () => {
-        dispatch(updateSliceName(slice, sliceName));
-        notify.success('This slice name was saved successfully.');
-      },
-      error: () => {
-        // if server-side reject the overwrite action,
-        // revert to old state
-        dispatch(updateSliceName(slice, oldName));
-        notify.error("You don't have the rights to alter this slice");
-      },
-    });
-  };
-}
-
-const FAVESTAR_BASE_URL = '/superset/favstar/Dashboard';
-export const TOGGLE_FAVE_STAR = 'TOGGLE_FAVE_STAR';
-export function toggleFaveStar(isStarred) {
-  return { type: TOGGLE_FAVE_STAR, isStarred };
-}
-
-export const FETCH_FAVE_STAR = 'FETCH_FAVE_STAR';
-export function fetchFaveStar(id) {
-  return function (dispatch) {
-    const url = `${FAVESTAR_BASE_URL}/${id}/count`;
-    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) {
-    const urlSuffix = isStarred ? 'unselect' : 'select';
-    const url = `${FAVESTAR_BASE_URL}/${id}/${urlSuffix}/`;
-    $.get(url);
-    dispatch(toggleFaveStar(!isStarred));
-  };
-}
-
-export const TOGGLE_EXPAND_SLICE = 'TOGGLE_EXPAND_SLICE';
-export function toggleExpandSlice(slice, isExpanded) {
-  return { type: TOGGLE_EXPAND_SLICE, slice, isExpanded };
-}
-
-export const SET_EDIT_MODE = 'SET_EDIT_MODE';
-export function setEditMode(editMode) {
-  return { type: SET_EDIT_MODE, editMode };
-}
diff --git a/superset/assets/src/dashboard/actions/dashboardState.js b/superset/assets/src/dashboard/actions/dashboardState.js
new file mode 100644
index 0000000..2262729
--- /dev/null
+++ b/superset/assets/src/dashboard/actions/dashboardState.js
@@ -0,0 +1,166 @@
+/* eslint camelcase: 0 */
+import $ from 'jquery';
+
+import { addChart, removeChart, refreshChart } from '../../chart/chartAction';
+import { chart as initChart } from '../../chart/chartReducer';
+import { fetchDatasourceMetadata } from '../../dashboard/actions/datasources';
+import { applyDefaultFormData } from '../../explore/stores/store';
+
+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 };
+}
+
+export const REMOVE_FILTER = 'REMOVE_FILTER';
+export function removeFilter(sliceId, col, vals, refresh = true) {
+  return { type: REMOVE_FILTER, sliceId, col, vals, refresh };
+}
+
+export const UPDATE_DASHBOARD_TITLE = 'UPDATE_DASHBOARD_TITLE';
+export function updateDashboardTitle(title) {
+  return { type: UPDATE_DASHBOARD_TITLE, title };
+}
+
+export const ADD_SLICE = 'ADD_SLICE';
+export function addSlice(slice) {
+  return { type: ADD_SLICE, slice };
+}
+
+export const REMOVE_SLICE = 'REMOVE_SLICE';
+export function removeSlice(sliceId) {
+  return { type: REMOVE_SLICE, sliceId };
+}
+
+const FAVESTAR_BASE_URL = '/superset/favstar/Dashboard';
+export const TOGGLE_FAVE_STAR = 'TOGGLE_FAVE_STAR';
+export function toggleFaveStar(isStarred) {
+  return { type: TOGGLE_FAVE_STAR, isStarred };
+}
+
+export const FETCH_FAVE_STAR = 'FETCH_FAVE_STAR';
+export function fetchFaveStar(id) {
+  return function (dispatch) {
+    const url = `${FAVESTAR_BASE_URL}/${id}/count`;
+    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) {
+    const urlSuffix = isStarred ? 'unselect' : 'select';
+    const url = `${FAVESTAR_BASE_URL}/${id}/${urlSuffix}/`;
+    $.get(url);
+    dispatch(toggleFaveStar(!isStarred));
+  };
+}
+
+export const TOGGLE_EXPAND_SLICE = 'TOGGLE_EXPAND_SLICE';
+export function toggleExpandSlice(sliceId) {
+  return { type: TOGGLE_EXPAND_SLICE, sliceId };
+}
+
+export const SET_EDIT_MODE = 'SET_EDIT_MODE';
+export function setEditMode(editMode) {
+  return { type: SET_EDIT_MODE, editMode };
+}
+
+export const ON_CHANGE = 'ON_CHANGE';
+export function onChange() {
+  return { type: ON_CHANGE };
+}
+
+export const ON_SAVE = 'ON_SAVE';
+export function onSave() {
+  return { type: ON_SAVE };
+}
+
+export function fetchCharts(chartList = [], force = false, interval = 0) {
+  return (dispatch, getState) => {
+    const timeout = getState().dashboardInfo.common.conf.SUPERSET_WEBSERVER_TIMEOUT;
+    if (!interval) {
+      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';
+    }
+    const delay = meta.stagger_refresh ? refreshTime / (chartList.length - 1) : 0;
+    chartList.forEach((chart, i) => {
+      setTimeout(() => dispatch(refreshChart(chart, force, timeout)), delay * i);
+    });
+  };
+}
+
+let refreshTimer = null;
+export function startPeriodicRender(interval) {
+  const stopPeriodicRender = () => {
+    if (refreshTimer) {
+      clearTimeout(refreshTimer);
+      refreshTimer = null;
+    }
+  };
+
+  return (dispatch, getState) => {
+    stopPeriodicRender();
+
+    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);
+      return dispatch(fetchCharts(affected, true, interval * 0.2));
+    };
+    const fetchAndRender = () => {
+      refreshAll();
+      if (interval > 0) {
+        refreshTimer = setTimeout(fetchAndRender, interval);
+      }
+    };
+
+    fetchAndRender();
+  };
+}
+
+export const TOGGLE_BUILDER_PANE = 'TOGGLE_BUILDER_PANE';
+export function toggleBuilderPane() {
+  return { type: TOGGLE_BUILDER_PANE };
+}
+
+export function addSliceToDashboard(id) {
+  return (dispatch, getState) => {
+    const { sliceEntities } = getState();
+    const selectedSlice = sliceEntities.slices[id];
+    const form_data = selectedSlice.form_data;
+    const newChart = {
+      ...initChart,
+      id,
+      form_data,
+      formData: applyDefaultFormData(form_data),
+    };
+
+    return Promise
+      .all([
+        dispatch(addChart(newChart, id)),
+        dispatch(fetchDatasourceMetadata(form_data.datasource)),
+      ])
+      .then(() => dispatch(addSlice(selectedSlice)));
+  };
+}
+
+export function removeSliceFromDashboard(chart) {
+  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
new file mode 100644
index 0000000..a00bb17
--- /dev/null
+++ b/superset/assets/src/dashboard/actions/datasources.js
@@ -0,0 +1,35 @@
+import $ from 'jquery';
+
+export const SET_DATASOURCE = 'SET_DATASOURCE';
+export function setDatasource(datasource, key) {
+  return { type: SET_DATASOURCE, datasource, key };
+}
+
+export const FETCH_DATASOURCE_STARTED = 'FETCH_DATASOURCE_STARTED';
+export function fetchDatasourceStarted(key) {
+  return { type: FETCH_DATASOURCE_STARTED, key };
+}
+
+export const FETCH_DATASOURCE_FAILED = 'FETCH_DATASOURCE_FAILED';
+export function fetchDatasourceFailed(error, key) {
+  return { type: FETCH_DATASOURCE_FAILED, error, key };
+}
+
+export function fetchDatasourceMetadata(key) {
+  return (dispatch, getState) => {
+    const { datasources } = getState();
+    const datasource = datasources[key];
+
+    if (datasource) {
+      return dispatch(setDatasource(datasource, key));
+    }
+
+    const url = `/superset/fetch_datasource_metadata?datasourceKey=${key}`;
+    return $.ajax({
+      type: 'GET',
+      url,
+      success: data => dispatch(setDatasource(data, key)),
+      error: error => dispatch(fetchDatasourceFailed(error.responseJSON.error, key)),
+    });
+  };
+}
diff --git a/superset/assets/src/dashboard/actions/sliceEntities.js b/superset/assets/src/dashboard/actions/sliceEntities.js
new file mode 100644
index 0000000..3a1b1dc
--- /dev/null
+++ b/superset/assets/src/dashboard/actions/sliceEntities.js
@@ -0,0 +1,93 @@
+/* eslint camelcase: 0 */
+/* global notify */
+import $ from 'jquery';
+
+export const UPDATE_SLICE_NAME = 'UPDATE_SLICE_NAME';
+export function updateSliceName(key, sliceName) {
+  return { type: UPDATE_SLICE_NAME, key, sliceName };
+}
+
+export function saveSliceName(slice, sliceName) {
+  const oldName = slice.slice_name;
+  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 key = slice.slice_id;
+    return $.ajax({
+      url,
+      type: 'POST',
+      success: () => {
+        dispatch(updateSliceName(key, sliceName));
+        notify.success('This slice name was saved successfully.');
+      },
+      error: () => {
+        // if server-side reject the overwrite action,
+        // revert to old state
+        dispatch(updateSliceName(key, oldName));
+        notify.error("You don't have the rights to alter this slice");
+      },
+    });
+  };
+}
+
+export const SET_ALL_SLICES = 'SET_ALL_SLICES';
+export function setAllSlices(slices) {
+  return { type: SET_ALL_SLICES, slices };
+}
+
+export const FETCH_ALL_SLICES_STARTED = 'FETCH_ALL_SLICES_STARTED';
+export function fetchAllSlicesStarted() {
+  return { type: FETCH_ALL_SLICES_STARTED };
+}
+
+export const FETCH_ALL_SLICES_FAILED = 'FETCH_ALL_SLICES_FAILED';
+export function fetchAllSlicesFailed(error) {
+  return { type: FETCH_ALL_SLICES_FAILED, error };
+}
+
+export function fetchAllSlices(userId) {
+  return (dispatch, getState) => {
+    const { sliceEntities }  = getState();
+    if (sliceEntities.lastUpdated === 0) {
+      dispatch(fetchAllSlicesStarted());
+
+      const uri = `/sliceaddview/api/read?_flt_0_created_by=${userId}`;
+      return $.ajax({
+        url: uri,
+        type: 'GET',
+        success: (response) => {
+          const slices = {};
+          response.result.forEach((slice) => {
+            const form_data = JSON.parse(slice.params);
+            slices[slice.id] = {
+              slice_id: slice.id,
+              slice_url: slice.slice_url,
+              slice_name: slice.slice_name,
+              edit_url: slice.edit_url,
+              form_data,
+              datasource: form_data.datasource,
+              datasource_name: slice.datasource_name_text,
+              datasource_link: slice.datasource_link,
+              changed_on: new Date(slice.changed_on).getTime(),
+              description: slice.description,
+              description_markdown: slice.description_markeddown,
+              viz_type: slice.viz_type,
+              modified: slice.modified,
+            };
+          });
+          return dispatch(setAllSlices(slices));
+        },
+        error: error => dispatch(fetchAllSlicesFailed(error)),
+      });
+    }
+
+    return dispatch(setAllSlices(sliceEntities.slices));
+  };
+}
diff --git a/superset/assets/src/dashboard/components/ActionMenuItem.jsx b/superset/assets/src/dashboard/components/ActionMenuItem.jsx
new file mode 100644
index 0000000..aaae4df
--- /dev/null
+++ b/superset/assets/src/dashboard/components/ActionMenuItem.jsx
@@ -0,0 +1,45 @@
+import React from 'react';
+import PropTypes from 'prop-types';
+import { MenuItem } from 'react-bootstrap';
+
+import InfoTooltipWithTrigger from '../../components/InfoTooltipWithTrigger';
+
+export function MenuItemContent({ faIcon, text, tooltip, children }) {
+  return (
+    <span>
+      {faIcon &&
+        <i className={`fa fa-${faIcon}`}>&nbsp;</i>
+      }
+      {text} {''}
+      <InfoTooltipWithTrigger
+        tooltip={tooltip}
+        label={faIcon ? `dash-${faIcon}` : ''}
+        placement="top"
+      />
+      {children}
+    </span>
+  );
+}
+MenuItemContent.propTypes = {
+  faIcon: PropTypes.string,
+  text: PropTypes.string,
+  tooltip: PropTypes.string,
+  children: PropTypes.node,
+};
+
+export function ActionMenuItem(props) {
+  return (
+    <MenuItem
+      onClick={props.onClick}
+      href={props.href}
+      target={props.target}
+    >
+      <MenuItemContent {...props} />
+    </MenuItem>
+  );
+}
+ActionMenuItem.propTypes = {
+  onClick: PropTypes.func,
+  href: PropTypes.string,
+  target: PropTypes.string,
+};
diff --git a/superset/assets/src/dashboard/components/Controls.jsx b/superset/assets/src/dashboard/components/Controls.jsx
index baad7bb..8755e8f 100644
--- a/superset/assets/src/dashboard/components/Controls.jsx
+++ b/superset/assets/src/dashboard/components/Controls.jsx
@@ -1,69 +1,34 @@
 import React from 'react';
 import PropTypes from 'prop-types';
-import { DropdownButton, MenuItem } from 'react-bootstrap';
+import $ from 'jquery';
+import { DropdownButton } from 'react-bootstrap';
 
-import CssEditor from './CssEditor';
 import RefreshIntervalModal from './RefreshIntervalModal';
 import SaveModal from './SaveModal';
-import SliceAdder from './SliceAdder';
+import { ActionMenuItem, MenuItemContent } from './ActionMenuItem';
 import { t } from '../../locales';
-import InfoTooltipWithTrigger from '../../components/InfoTooltipWithTrigger';
-
-const $ = window.$ = require('jquery');
 
 const propTypes = {
-  dashboard: PropTypes.object.isRequired,
+  dashboardInfo: PropTypes.object.isRequired,
+  dashboardTitle: PropTypes.string.isRequired,
+  layout: PropTypes.object.isRequired,
   filters: PropTypes.object.isRequired,
+  expandedSlices: PropTypes.object.isRequired,
   slices: PropTypes.array,
-  userId: PropTypes.string.isRequired,
-  addSlicesToDashboard: PropTypes.func,
   onSave: PropTypes.func,
   onChange: PropTypes.func,
-  renderSlices: PropTypes.func,
-  serialize: PropTypes.func,
+  forceRefreshAllCharts: PropTypes.func,
   startPeriodicRender: PropTypes.func,
   editMode: PropTypes.bool,
 };
 
-function MenuItemContent({ faIcon, text, tooltip, children }) {
-  return (
-    <span>
-      <i className={`fa fa-${faIcon}`} /> {text} {''}
-      <InfoTooltipWithTrigger
-        tooltip={tooltip}
-        label={`dash-${faIcon}`}
-        placement="top"
-      />
-      {children}
-    </span>
-  );
-}
-MenuItemContent.propTypes = {
-  faIcon: PropTypes.string.isRequired,
-  text: PropTypes.string,
-  tooltip: PropTypes.string,
-  children: PropTypes.node,
-};
-
-function ActionMenuItem(props) {
-  return (
-    <MenuItem onClick={props.onClick}>
-      <MenuItemContent {...props} />
-    </MenuItem>
-  );
-}
-ActionMenuItem.propTypes = {
-  onClick: PropTypes.func,
-};
-
 class Controls extends React.PureComponent {
   constructor(props) {
     super(props);
     this.state = {
-      css: props.dashboard.css || '',
+      css: '',
       cssTemplates: [],
     };
-    this.refresh = this.refresh.bind(this);
     this.toggleModal = this.toggleModal.bind(this);
     this.updateDom = this.updateDom.bind(this);
   }
@@ -79,10 +44,6 @@ class Controls extends React.PureComponent {
       this.setState({ cssTemplates });
     });
   }
-  refresh() {
-    // Force refresh all slices
-    this.props.renderSlices(true);
-  }
   toggleModal(modal) {
     let currentModal;
     if (modal !== this.state.currentModal) {
@@ -114,12 +75,12 @@ class Controls extends React.PureComponent {
     }
   }
   render() {
-    const { dashboard, userId, filters,
-      addSlicesToDashboard, startPeriodicRender,
-      serialize, 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'
-      + `${dashboard.dashboard_title}&Body=${emailBody}`;
+      + `${dashboardTitle}&Body=${emailBody}`;
     let saveText = t('Save as');
     if (editMode) {
       saveText = t('Save');
@@ -130,8 +91,7 @@ class Controls extends React.PureComponent {
           <ActionMenuItem
             text={t('Force Refresh')}
             tooltip={t('Force refresh the whole dashboard')}
-            faIcon="refresh"
-            onClick={this.refresh}
+            onClick={forceRefreshAllCharts}
           />
           <RefreshIntervalModal
             onChange={refreshInterval => startPeriodicRender(refreshInterval * 1000)}
@@ -139,32 +99,29 @@ class Controls extends React.PureComponent {
               <MenuItemContent
                 text={t('Set autorefresh')}
                 tooltip={t('Set the auto-refresh interval for this session')}
-                faIcon="clock-o"
               />
             }
           />
-          {dashboard.dash_save_perm &&
-            <SaveModal
-              dashboard={dashboard}
-              filters={filters}
-              serialize={serialize}
-              onSave={onSave}
-              css={this.state.css}
-              triggerNode={
-                <MenuItemContent
-                  text={saveText}
-                  tooltip={t('Save the dashboard')}
-                  faIcon="save"
-                />
-              }
-            />
-          }
+          <SaveModal
+            dashboardId={this.props.dashboardInfo.id}
+            dashboardTitle={dashboardTitle}
+            layout={layout}
+            filters={filters}
+            expandedSlices={expandedSlices}
+            onSave={onSave}
+            css={this.state.css}
+            triggerNode={
+              <MenuItemContent
+                text={saveText}
+                tooltip={t('Save the dashboard')}
+              />
+            }
+          />
           {editMode &&
             <ActionMenuItem
               text={t('Edit properties')}
               tooltip={t("Edit the dashboards's properties")}
-              faIcon="edit"
-              onClick={() => { window.location = `/dashboardmodelview/edit/${dashboard.id}`; }}
+              onClick={() => { window.location = `/dashboardmodelview/edit/${this.props.dashboardInfo.id}`; }}
             />
           }
           {editMode &&
@@ -172,36 +129,6 @@ class Controls extends React.PureComponent {
               text={t('Email')}
               tooltip={t('Email a link to this dashboard')}
               onClick={() => { window.location = emailLink; }}
-              faIcon="envelope"
-            />
-          }
-          {editMode &&
-            <SliceAdder
-              dashboard={dashboard}
-              addSlicesToDashboard={addSlicesToDashboard}
-              userId={userId}
-              triggerNode={
-                <MenuItemContent
-                  text={t('Add Charts')}
-                  tooltip={t('Add some charts to this dashboard')}
-                  faIcon="plus"
-                />
-              }
-            />
-          }
-          {editMode &&
-            <CssEditor
-              dashboard={dashboard}
-              triggerNode={
-                <MenuItemContent
-                  text={t('Edit CSS')}
-                  tooltip={t('Change the style of the dashboard using CSS code')}
-                  faIcon="css3"
-                />
-              }
-              initialCss={this.state.css}
-              templates={this.state.cssTemplates}
-              onChange={this.changeCss.bind(this)}
             />
           }
         </DropdownButton>
diff --git a/superset/assets/src/dashboard/components/Dashboard.jsx b/superset/assets/src/dashboard/components/Dashboard.jsx
index 2cb08c3..939476c 100644
--- a/superset/assets/src/dashboard/components/Dashboard.jsx
+++ b/superset/assets/src/dashboard/components/Dashboard.jsx
@@ -3,81 +3,69 @@ import PropTypes from 'prop-types';
 
 import AlertsWrapper from '../../components/AlertsWrapper';
 import GridLayout from './GridLayout';
-import Header from './Header';
+import {
+  chartPropShape,
+  slicePropShape,
+  dashboardInfoPropShape,
+  dashboardStatePropShape,
+} from '../v2/util/propShapes';
 import { exportChart } from '../../explore/exploreUtils';
 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 { t } from '../../locales';
 
-import '../../../stylesheets/dashboard.css';
+import '../../../stylesheets/dashboard.less';
+import '../v2/stylesheets/index.less';
 
 const propTypes = {
-  actions: PropTypes.object,
+  actions: PropTypes.object.isRequired,
+  dashboardInfo: dashboardInfoPropShape.isRequired,
+  dashboardState: dashboardStatePropShape.isRequired,
+  charts: PropTypes.objectOf(chartPropShape).isRequired,
+  slices: PropTypes.objectOf(slicePropShape).isRequired,
+  datasources: PropTypes.object.isRequired,
+  layout: PropTypes.object.isRequired,
+  impressionId: PropTypes.string.isRequired,
   initMessages: PropTypes.array,
-  dashboard: PropTypes.object.isRequired,
-  slices: PropTypes.object,
-  datasources: PropTypes.object,
-  filters: PropTypes.object,
-  refresh: PropTypes.bool,
   timeout: PropTypes.number,
   userId: PropTypes.string,
-  isStarred: PropTypes.bool,
-  editMode: PropTypes.bool,
-  impressionId: PropTypes.string,
 };
 
 const defaultProps = {
   initMessages: [],
-  dashboard: {},
-  slices: {},
-  datasources: {},
-  filters: {},
-  refresh: false,
   timeout: 60,
   userId: '',
-  isStarred: false,
-  editMode: false,
 };
 
 class Dashboard extends React.PureComponent {
   constructor(props) {
     super(props);
-    this.refreshTimer = null;
+
     this.firstLoad = true;
     this.loadingLog = new ActionLog({
       impressionId: props.impressionId,
       actionType: LOG_ACTIONS_PAGE_LOAD,
       source: 'dashboard',
-      sourceId: props.dashboard.id,
+      sourceId: props.dashboardInfo.id,
       eventNames: [LOG_ACTIONS_LOAD_EVENT, LOG_ACTIONS_RENDER_EVENT],
     });
     Logger.start(this.loadingLog);
 
-    // alert for unsaved changes
-    this.state = { unsavedChanges: false };
-
     this.rerenderCharts = this.rerenderCharts.bind(this);
-    this.updateDashboardTitle = this.updateDashboardTitle.bind(this);
-    this.onSave = this.onSave.bind(this);
-    this.onChange = this.onChange.bind(this);
-    this.serialize = this.serialize.bind(this);
-    this.fetchAllSlices = this.fetchSlices.bind(this, this.getAllSlices());
-    this.startPeriodicRender = this.startPeriodicRender.bind(this);
-    this.addSlicesToDashboard = this.addSlicesToDashboard.bind(this);
-    this.fetchSlice = this.fetchSlice.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.fetchFaveStar = this.props.actions.fetchFaveStar.bind(this);
-    this.props.actions.saveFaveStar = this.props.actions.saveFaveStar.bind(this);
-    this.props.actions.saveSlice = this.props.actions.saveSlice.bind(this);
-    this.props.actions.removeSlice = this.props.actions.removeSlice.bind(this);
-    this.props.actions.removeChart = this.props.actions.removeChart.bind(this);
-    this.props.actions.updateDashboardLayout = this.props.actions.updateDashboardLayout.bind(this);
-    this.props.actions.toggleExpandSlice = this.props.actions.toggleExpandSlice.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.clearFilter = this.props.actions.clearFilter.bind(this);
     this.props.actions.removeFilter = this.props.actions.removeFilter.bind(this);
   }
 
@@ -87,22 +75,37 @@ class Dashboard extends React.PureComponent {
 
   componentWillReceiveProps(nextProps) {
     if (this.firstLoad &&
-      Object.values(nextProps.slices)
-        .every(slice => (['rendered', 'failed', 'stopped'].indexOf(slice.chartStatus) > -1))
+      Object.values(nextProps.charts)
+        .every(chart => (['rendered', 'failed', 'stopped'].indexOf(chart.chartStatus) > -1))
     ) {
       Logger.end(this.loadingLog);
       this.firstLoad = false;
     }
+
+    const currentChartIds = getChartIdsFromLayout(this.props.layout);
+    const nextChartIds = getChartIdsFromLayout(nextProps.layout);
+    if (currentChartIds.length < nextChartIds.length) {
+      // adding new chart
+      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]);
+      this.props.actions.onChange();
+    }
   }
 
   componentDidUpdate(prevProps) {
-    if (this.props.refresh) {
+    const { refresh, filters, hasUnsavedChanges } = this.props.dashboardState;
+    if (refresh) {
       let changedFilterKey;
-      const prevFiltersKeySet = new Set(Object.keys(prevProps.filters));
-      Object.keys(this.props.filters).some((key) => {
+      const prevFiltersKeySet = new Set(Object.keys(prevProps.dashboardState.filters));
+      Object.keys(filters).some((key) => {
         prevFiltersKeySet.delete(key);
-        if (prevProps.filters[key] === undefined ||
-          !areObjectsEqual(prevProps.filters[key], this.props.filters[key])) {
+        if (prevProps.dashboardState.filters[key] === undefined ||
+          !areObjectsEqual(prevProps.dashboardState.filters[key], filters[key])) {
           changedFilterKey = key;
           return true;
         }
@@ -113,6 +116,12 @@ class Dashboard extends React.PureComponent {
         this.refreshExcept(changedFilterKey);
       }
     }
+
+    if (hasUnsavedChanges) {
+      this.onBeforeUnload(true);
+    } else {
+      this.onBeforeUnload(false);
+    }
   }
 
   componentWillUnmount() {
@@ -127,29 +136,22 @@ class Dashboard extends React.PureComponent {
     }
   }
 
-  onChange() {
-    this.onBeforeUnload(true);
-    this.setState({ unsavedChanges: true });
-  }
-
-  onSave() {
-    this.onBeforeUnload(false);
-    this.setState({ unsavedChanges: false });
-  }
-
   // return charts in array
-  getAllSlices() {
-    return Object.values(this.props.slices);
+  getAllCharts() {
+    return Object.values(this.props.charts);
   }
 
-  getFormDataExtra(slice) {
-    const formDataExtra = Object.assign({}, slice.formData);
-    formDataExtra.extra_filters = this.effectiveExtraFilters(slice.slice_id);
+  getFormDataExtra(chart) {
+    const extraFilters = this.effectiveExtraFilters(chart.id);
+    const formDataExtra = {
+      ...chart.formData,
+      extra_filters: extraFilters,
+    };
     return formDataExtra;
   }
 
   getFilters(sliceId) {
-    return this.props.filters[sliceId];
+    return this.props.dashboardState.filters[sliceId];
   }
 
   unload() {
@@ -159,8 +161,8 @@ class Dashboard extends React.PureComponent {
   }
 
   effectiveExtraFilters(sliceId) {
-    const metadata = this.props.dashboard.metadata;
-    const filters = this.props.filters;
+    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)) {
@@ -195,154 +197,75 @@ class Dashboard extends React.PureComponent {
   }
 
   refreshExcept(filterKey) {
-    const immune = this.props.dashboard.metadata.filter_immune_slices || [];
-    let slices = this.getAllSlices();
+    const immune = this.props.dashboardInfo.metadata.filter_immune_slices || [];
+    let charts = this.getAllCharts();
     if (filterKey) {
-      slices = slices.filter(slice => (
-        String(slice.slice_id) !== filterKey &&
-        immune.indexOf(slice.slice_id) === -1
-      ));
-    }
-    this.fetchSlices(slices);
-  }
-
-  stopPeriodicRender() {
-    if (this.refreshTimer) {
-      clearTimeout(this.refreshTimer);
-      this.refreshTimer = null;
-    }
-  }
-
-  startPeriodicRender(interval) {
-    this.stopPeriodicRender();
-    const immune = this.props.dashboard.metadata.timed_refresh_immune_slices || [];
-    const refreshAll = () => {
-      const affectedSlices = this.getAllSlices()
-        .filter(slice => immune.indexOf(slice.slice_id) === -1);
-      this.fetchSlices(affectedSlices, true, interval * 0.2);
-    };
-    const fetchAndRender = () => {
-      refreshAll();
-      if (interval > 0) {
-        this.refreshTimer = setTimeout(fetchAndRender, interval);
-      }
-    };
-
-    fetchAndRender();
-  }
-
-  updateDashboardTitle(title) {
-    this.props.actions.updateDashboardTitle(title);
-    this.onChange();
-  }
-
-  serialize() {
-    return this.props.dashboard.layout.map(reactPos => ({
-      slice_id: reactPos.i,
-      col: reactPos.x + 1,
-      row: reactPos.y,
-      size_x: reactPos.w,
-      size_y: reactPos.h,
-    }));
-  }
-
-  addSlicesToDashboard(sliceIds) {
-    return this.props.actions.addSlicesToDashboard(this.props.dashboard.id, sliceIds);
-  }
-
-  fetchSlice(slice, force = false) {
-    return this.props.actions.runQuery(
-      this.getFormDataExtra(slice), force, this.props.timeout, slice.chartKey,
-    );
-  }
-
-  // fetch and render an list of slices
-  fetchSlices(slc, force = false, interval = 0) {
-    const slices = slc || this.getAllSlices();
-    if (!interval) {
-      slices.forEach((slice) => { this.fetchSlice(slice, force); });
-      return;
+      charts = charts.filter(
+        chart => (String(chart.id) !== filterKey && immune.indexOf(chart.id) === -1),
+      );
     }
-
-    const meta = this.props.dashboard.metadata;
-    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';
-    }
-    const delay = meta.stagger_refresh ? refreshTime / (slices.length - 1) : 0;
-    slices.forEach((slice, i) => {
-      setTimeout(() => { this.fetchSlice(slice, force); }, delay * i);
+    charts.forEach((chart) => {
+      const updatedFormData = this.getFormDataExtra(chart);
+      this.props.actions.runQuery(updatedFormData, false, this.props.timeout, chart.id);
     });
   }
 
-  exploreChart(slice) {
-    const formData = this.getFormDataExtra(slice);
+  exploreChart(chartId) {
+    const chart = this.props.charts[chartId];
+    const formData = this.getFormDataExtra(chart);
     exportChart(formData);
   }
 
-  exportCSV(slice) {
-    const formData = this.getFormDataExtra(slice);
+  exportCSV(chartId) {
+    const chart = this.props.charts[chartId];
+    const formData = this.getFormDataExtra(chart);
     exportChart(formData, 'csv');
   }
 
   // re-render chart without fetch
   rerenderCharts() {
-    this.getAllSlices().forEach((slice) => {
+    this.getAllCharts().forEach((chart) => {
       setTimeout(() => {
-        this.props.actions.renderTriggered(new Date().getTime(), slice.chartKey);
+        this.props.actions.renderTriggered(new Date().getTime(), chart.id);
       }, 50);
     });
   }
 
   render() {
+    const {
+      expandedSlices = {}, filters, sliceIds,
+      editMode, showBuilderPane,
+    } = this.props.dashboardState;
+
     return (
       <div id="dashboard-container">
-        <div id="dashboard-header">
+        <div>
           <AlertsWrapper initMessages={this.props.initMessages} />
-          <Header
-            dashboard={this.props.dashboard}
-            unsavedChanges={this.state.unsavedChanges}
-            filters={this.props.filters}
-            userId={this.props.userId}
-            isStarred={this.props.isStarred}
-            updateDashboardTitle={this.updateDashboardTitle}
-            onSave={this.onSave}
-            onChange={this.onChange}
-            serialize={this.serialize}
-            fetchFaveStar={this.props.actions.fetchFaveStar}
-            saveFaveStar={this.props.actions.saveFaveStar}
-            renderSlices={this.fetchAllSlices}
-            startPeriodicRender={this.startPeriodicRender}
-            addSlicesToDashboard={this.addSlicesToDashboard}
-            editMode={this.props.editMode}
-            setEditMode={this.props.actions.setEditMode}
-          />
-        </div>
-        <div id="grid-container" className="slice-grid gridster">
-          <GridLayout
-            dashboard={this.props.dashboard}
-            datasources={this.props.datasources}
-            filters={this.props.filters}
-            charts={this.props.slices}
-            timeout={this.props.timeout}
-            onChange={this.onChange}
-            getFormDataExtra={this.getFormDataExtra}
-            exploreChart={this.exploreChart}
-            exportCSV={this.exportCSV}
-            fetchSlice={this.fetchSlice}
-            saveSlice={this.props.actions.saveSlice}
-            removeSlice={this.props.actions.removeSlice}
-            removeChart={this.props.actions.removeChart}
-            updateDashboardLayout={this.props.actions.updateDashboardLayout}
-            toggleExpandSlice={this.props.actions.toggleExpandSlice}
-            addFilter={this.props.actions.addFilter}
-            getFilters={this.getFilters}
-            clearFilter={this.props.actions.clearFilter}
-            removeFilter={this.props.actions.removeFilter}
-            editMode={this.props.editMode}
-          />
         </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>
     );
   }
diff --git a/superset/assets/src/dashboard/components/DashboardContainer.jsx b/superset/assets/src/dashboard/components/DashboardContainer.jsx
index d429461..31fe035 100644
--- a/superset/assets/src/dashboard/components/DashboardContainer.jsx
+++ b/superset/assets/src/dashboard/components/DashboardContainer.jsx
@@ -1,28 +1,48 @@
 import { bindActionCreators } from 'redux';
 import { connect } from 'react-redux';
 
-import * as dashboardActions from '../actions';
-import * as chartActions from '../../chart/chartAction';
-import Dashboard from '../v2/components/Dashboard';
+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(/* { charts, dashboard, impressionId } */) {
+function mapStateToProps({ datasources, sliceEntities, charts,
+                           dashboardInfo, dashboardState,
+                           dashboardLayout, impressionId }) {
   return {
-    // initMessages: dashboard.common.flash_messages,
-    // timeout: dashboard.common.conf.SUPERSET_WEBSERVER_TIMEOUT,
-    // dashboard: dashboard.dashboard,
-    // slices: charts,
-    // datasources: dashboard.datasources,
-    // filters: dashboard.filters,
-    // refresh: !!dashboard.refresh,
-    // userId: dashboard.userId,
-    // isStarred: !!dashboard.isStarred,
-    // editMode: dashboard.editMode,
-    // impressionId,
+    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 = { ...chartActions, ...dashboardActions };
+  const actions = {
+    refreshChart,
+    runQuery,
+    renderTriggered,
+    saveSliceName,
+    toggleExpandSlice,
+    addFilter,
+    removeFilter,
+    addSliceToDashboard,
+    removeSliceFromDashboard,
+    onChange,
+  };
   return {
     actions: bindActionCreators(actions, dispatch),
   };
diff --git a/superset/assets/src/dashboard/components/GridCell.jsx b/superset/assets/src/dashboard/components/GridCell.jsx
index 91fe839..3273272 100644
--- a/superset/assets/src/dashboard/components/GridCell.jsx
+++ b/superset/assets/src/dashboard/components/GridCell.jsx
@@ -4,8 +4,7 @@ import PropTypes from 'prop-types';
 
 import SliceHeader from './SliceHeader';
 import ChartContainer from '../../chart/ChartContainer';
-
-import '../../../stylesheets/dashboard.css';
+import { chartPropShape, slicePropShape } from '../v2/util/propShapes';
 
 const propTypes = {
   timeout: PropTypes.number,
@@ -16,34 +15,30 @@ const propTypes = {
   isExpanded: PropTypes.bool,
   widgetHeight: PropTypes.number,
   widgetWidth: PropTypes.number,
-  slice: PropTypes.object,
-  chartKey: PropTypes.string,
+  slice: slicePropShape.isRequired,
+  chart: chartPropShape.isRequired,
   formData: PropTypes.object,
   filters: PropTypes.object,
-  forceRefresh: PropTypes.func,
-  removeSlice: PropTypes.func,
+  refreshChart: PropTypes.func,
   updateSliceName: PropTypes.func,
   toggleExpandSlice: PropTypes.func,
   exploreChart: PropTypes.func,
   exportCSV: PropTypes.func,
   addFilter: PropTypes.func,
   getFilters: PropTypes.func,
-  clearFilter: PropTypes.func,
   removeFilter: PropTypes.func,
   editMode: PropTypes.bool,
   annotationQuery: PropTypes.object,
 };
 
 const defaultProps = {
-  forceRefresh: () => ({}),
-  removeSlice: () => ({}),
+  refreshChart: () => ({}),
   updateSliceName: () => ({}),
   toggleExpandSlice: () => ({}),
   exploreChart: () => ({}),
   exportCSV: () => ({}),
   addFilter: () => ({}),
   getFilters: () => ({}),
-  clearFilter: () => ({}),
   removeFilter: () => ({}),
   editMode: false,
 };
@@ -53,9 +48,9 @@ class GridCell extends React.PureComponent {
     super(props);
 
     const sliceId = this.props.slice.slice_id;
-    this.addFilter = this.props.addFilter.bind(this, sliceId);
+    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.clearFilter = this.props.clearFilter.bind(this, sliceId);
     this.removeFilter = this.props.removeFilter.bind(this, sliceId);
   }
 
@@ -68,7 +63,7 @@ class GridCell extends React.PureComponent {
   }
 
   width() {
-    return this.props.widgetWidth - 10;
+    return this.props.widgetWidth - 32;
   }
 
   height(slice) {
@@ -80,7 +75,7 @@ class GridCell extends React.PureComponent {
       descriptionHeight = this.refs[descriptionId].offsetHeight + 10;
     }
 
-    return widgetHeight - headerHeight - descriptionHeight;
+    return widgetHeight - headerHeight - descriptionHeight - 32;
   }
 
   headerHeight(slice) {
@@ -88,13 +83,18 @@ class GridCell extends React.PureComponent {
     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,
-      removeSlice, updateSliceName, toggleExpandSlice, forceRefresh,
-      chartKey, slice, datasource, formData, timeout, annotationQuery,
-      exploreChart, exportCSV,
+      updateSliceName, toggleExpandSlice,
+      chart, slice, datasource, formData, timeout, annotationQuery,
+      exploreChart, exportCSV, editMode,
     } = this.props;
+
     return (
       <div
         className={isLoading ? 'slice-cell-highlight' : 'slice-cell'}
@@ -106,11 +106,10 @@ class GridCell extends React.PureComponent {
             isExpanded={isExpanded}
             isCached={isCached}
             cachedDttm={cachedDttm}
-            removeSlice={removeSlice}
             updateSliceName={updateSliceName}
             toggleExpandSlice={toggleExpandSlice}
-            forceRefresh={forceRefresh}
-            editMode={this.props.editMode}
+            forceRefresh={this.forceRefresh}
+            editMode={editMode}
             annotationQuery={annotationQuery}
             exploreChart={exploreChart}
             exportCSV={exportCSV}
@@ -128,21 +127,23 @@ class GridCell extends React.PureComponent {
           ref={this.getDescriptionId(slice)}
           dangerouslySetInnerHTML={{ __html: slice.description_markeddown }}
         />
-        <div className="row chart-container">
+        <div
+          className="chart-container"
+          style={{ width: this.width(), height: this.height(slice) }}
+        >
           <input type="hidden" value="false" />
           <ChartContainer
             containerId={`slice-container-${slice.slice_id}`}
-            chartKey={chartKey}
+            chartId={chart.id}
             datasource={datasource}
             formData={formData}
             headerHeight={this.headerHeight(slice)}
             height={this.height(slice)}
             width={this.width()}
             timeout={timeout}
-            vizType={slice.formData.viz_type}
+            vizType={slice.viz_type}
             addFilter={this.addFilter}
             getFilters={this.getFilters}
-            clearFilter={this.clearFilter}
             removeFilter={this.removeFilter}
           />
         </div>
diff --git a/superset/assets/src/dashboard/components/GridLayout.jsx b/superset/assets/src/dashboard/components/GridLayout.jsx
index ef0ec24..fd561e2 100644
--- a/superset/assets/src/dashboard/components/GridLayout.jsx
+++ b/superset/assets/src/dashboard/components/GridLayout.jsx
@@ -1,51 +1,49 @@
 import React from 'react';
 import PropTypes from 'prop-types';
-import { Responsive, WidthProvider } from 'react-grid-layout';
+import cx from 'classnames';
 
 import GridCell from './GridCell';
-
-require('react-grid-layout/css/styles.css');
-require('react-resizable/css/styles.css');
-
-const ResponsiveReactGridLayout = WidthProvider(Responsive);
+import { slicePropShape, chartPropShape } from '../v2/util/propShapes';
+import DashboardBuilder from '../v2/containers/DashboardBuilder';
 
 const propTypes = {
-  dashboard: PropTypes.object.isRequired,
+  dashboardInfo: PropTypes.shape().isRequired,
+  layout: PropTypes.object.isRequired,
   datasources: PropTypes.object,
-  charts: PropTypes.object.isRequired,
+  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,
-  fetchSlice: PropTypes.func,
-  saveSlice: PropTypes.func,
-  removeSlice: PropTypes.func,
-  removeChart: PropTypes.func,
-  updateDashboardLayout: PropTypes.func,
+  refreshChart: PropTypes.func,
+  saveSliceName: PropTypes.func,
   toggleExpandSlice: PropTypes.func,
   addFilter: PropTypes.func,
   getFilters: PropTypes.func,
-  clearFilter: PropTypes.func,
   removeFilter: PropTypes.func,
   editMode: PropTypes.bool.isRequired,
+  showBuilderPane: PropTypes.bool.isRequired,
 };
 
 const defaultProps = {
+  expandedSlices: {},
+  filters: {},
+  timeout: 60,
   onChange: () => ({}),
   getFormDataExtra: () => ({}),
   exploreChart: () => ({}),
   exportCSV: () => ({}),
-  fetchSlice: () => ({}),
-  saveSlice: () => ({}),
-  removeSlice: () => ({}),
-  removeChart: () => ({}),
-  updateDashboardLayout: () => ({}),
+  refreshChart: () => ({}),
+  saveSliceName: () => ({}),
   toggleExpandSlice: () => ({}),
   addFilter: () => ({}),
   getFilters: () => ({}),
-  clearFilter: () => ({}),
   removeFilter: () => ({}),
 };
 
@@ -53,141 +51,101 @@ class GridLayout extends React.Component {
   constructor(props) {
     super(props);
 
-    this.onResizeStop = this.onResizeStop.bind(this);
-    this.onDragStop = this.onDragStop.bind(this);
-    this.forceRefresh = this.forceRefresh.bind(this);
-    this.removeSlice = this.removeSlice.bind(this);
-    this.updateSliceName = this.props.dashboard.dash_edit_perm ?
+    this.updateSliceName = this.props.dashboardInfo.dash_edit_perm ?
       this.updateSliceName.bind(this) : null;
   }
 
-  onResizeStop(layout) {
-    this.props.updateDashboardLayout(layout);
-    this.props.onChange();
-  }
-
-  onDragStop(layout) {
-    this.props.updateDashboardLayout(layout);
-    this.props.onChange();
+  componentDidUpdate(prevProps) {
+    if (prevProps.editMode !== this.props.editMode ||
+      prevProps.showBuilderPane !== this.props.showBuilderPane) {
+      this.props.rerenderCharts();
+    }
   }
 
-  getWidgetId(slice) {
-    return 'widget_' + slice.slice_id;
+  getWidgetId(sliceId) {
+    return 'widget_' + sliceId;
   }
 
-  getWidgetHeight(slice) {
-    const widgetId = this.getWidgetId(slice);
+  getWidgetHeight(sliceId) {
+    const widgetId = this.getWidgetId(sliceId);
     if (!widgetId || !this.refs[widgetId]) {
       return 400;
     }
-    return this.refs[widgetId].offsetHeight;
+    return this.refs[widgetId].parentNode.clientHeight;
   }
 
-  getWidgetWidth(slice) {
-    const widgetId = this.getWidgetId(slice);
+  getWidgetWidth(sliceId) {
+    const widgetId = this.getWidgetId(sliceId);
     if (!widgetId || !this.refs[widgetId]) {
       return 400;
     }
-    return this.refs[widgetId].offsetWidth;
-  }
-
-  findSliceIndexById(sliceId) {
-    return this.props.dashboard.slices
-      .map(slice => (slice.slice_id)).indexOf(sliceId);
-  }
-
-  forceRefresh(sliceId) {
-    return this.props.fetchSlice(this.props.charts['slice_' + sliceId], true);
-  }
-
-  removeSlice(slice) {
-    if (!slice) {
-      return;
-    }
-
-    // remove slice dashboard and charts
-    this.props.removeSlice(slice);
-    this.props.removeChart(this.props.charts['slice_' + slice.slice_id].chartKey);
-    this.props.onChange();
+    return this.refs[widgetId].parentNode.clientWidth;
   }
 
   updateSliceName(sliceId, sliceName) {
-    const index = this.findSliceIndexById(sliceId);
-    if (index === -1) {
-      return;
-    }
-
-    const currentSlice = this.props.dashboard.slices[index];
-    if (currentSlice.slice_name === sliceName) {
+    const key = sliceId;
+    const currentSlice = this.props.slices[key];
+    if (!currentSlice || currentSlice.slice_name === sliceName) {
       return;
     }
 
-    this.props.saveSlice(currentSlice, sliceName);
+    this.props.saveSliceName(currentSlice, sliceName);
   }
 
-  isExpanded(slice) {
-    return this.props.dashboard.metadata.expanded_slices &&
-      this.props.dashboard.metadata.expanded_slices[slice.slice_id];
+  isExpanded(sliceId) {
+    return this.props.expandedSlices[sliceId];
   }
 
   render() {
-    const cells = this.props.dashboard.slices.map((slice) => {
-      const chartKey = `slice_${slice.slice_id}`;
-      const currentChart = this.props.charts[chartKey];
-      const queryResponse = currentChart.queryResponse || {};
-      return (
-        <div
-          id={'slice_' + slice.slice_id}
-          key={slice.slice_id}
-          data-slice-id={slice.slice_id}
-          className={`widget ${slice.form_data.viz_type}`}
-          ref={this.getWidgetId(slice)}
-        >
-          <GridCell
-            slice={slice}
-            chartKey={chartKey}
-            datasource={this.props.datasources[slice.form_data.datasource]}
-            filters={this.props.filters}
-            formData={this.props.getFormDataExtra(slice)}
-            timeout={this.props.timeout}
-            widgetHeight={this.getWidgetHeight(slice)}
-            widgetWidth={this.getWidgetWidth(slice)}
-            exploreChart={this.props.exploreChart}
-            exportCSV={this.props.exportCSV}
-            isExpanded={!!this.isExpanded(slice)}
-            isLoading={currentChart.chartStatus === 'loading'}
-            isCached={queryResponse.is_cached}
-            cachedDttm={queryResponse.cached_dttm}
-            toggleExpandSlice={this.props.toggleExpandSlice}
-            forceRefresh={this.forceRefresh}
-            removeSlice={this.removeSlice}
-            updateSliceName={this.updateSliceName}
-            addFilter={this.props.addFilter}
-            getFilters={this.props.getFilters}
-            clearFilter={this.props.clearFilter}
-            removeFilter={this.props.removeFilter}
-            editMode={this.props.editMode}
-            annotationQuery={currentChart.annotationQuery}
-            annotationError={currentChart.annotationError}
-          />
-        </div>);
+    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 (
-      <ResponsiveReactGridLayout
-        className="layout"
-        layouts={{ lg: this.props.dashboard.layout }}
-        onResizeStop={this.onResizeStop}
-        onDragStop={this.onDragStop}
-        cols={{ lg: 48, md: 48, sm: 40, xs: 32, xxs: 24 }}
-        rowHeight={10}
-        autoSize
-        margin={[20, 20]}
-        useCSSTransforms
-        draggableHandle=".drag"
-      >
-        {cells}
-      </ResponsiveReactGridLayout>
+      <DashboardBuilder
+        cells={cells}
+      />
     );
   }
 }
diff --git a/superset/assets/src/dashboard/components/Header.jsx b/superset/assets/src/dashboard/components/Header.jsx
index eabd3f4..f533506 100644
--- a/superset/assets/src/dashboard/components/Header.jsx
+++ b/superset/assets/src/dashboard/components/Header.jsx
@@ -1,47 +1,65 @@
 import React from 'react';
 import PropTypes from 'prop-types';
+import { ButtonGroup, ButtonToolbar } from 'react-bootstrap';
 
 import Controls from './Controls';
 import EditableTitle from '../../components/EditableTitle';
 import Button from '../../components/Button';
 import FaveStar from '../../components/FaveStar';
-import URLShortLinkButton from '../../components/URLShortLinkButton';
 import InfoTooltipWithTrigger from '../../components/InfoTooltipWithTrigger';
+import { chartPropShape } from '../v2/util/propShapes';
 import { t } from '../../locales';
 
 const propTypes = {
-  dashboard: PropTypes.object.isRequired,
+  dashboardInfo: PropTypes.object.isRequired,
+  dashboardTitle: PropTypes.string.isRequired,
+  charts: PropTypes.objectOf(chartPropShape).isRequired,
+  layout: PropTypes.object.isRequired,
   filters: PropTypes.object.isRequired,
-  userId: PropTypes.string.isRequired,
-  isStarred: PropTypes.bool,
-  addSlicesToDashboard: PropTypes.func,
-  onSave: PropTypes.func,
-  onChange: PropTypes.func,
+  expandedSlices: PropTypes.object.isRequired,
+  isStarred: PropTypes.bool.isRequired,
+  onSave: PropTypes.func.isRequired,
+  onChange: PropTypes.func.isRequired,
   fetchFaveStar: PropTypes.func,
-  renderSlices: PropTypes.func,
+  fetchCharts: PropTypes.func.isRequired,
   saveFaveStar: PropTypes.func,
-  serialize: PropTypes.func,
-  startPeriodicRender: PropTypes.func,
-  updateDashboardTitle: PropTypes.func,
+  startPeriodicRender: PropTypes.func.isRequired,
+  updateDashboardTitle: PropTypes.func.isRequired,
   editMode: PropTypes.bool.isRequired,
   setEditMode: PropTypes.func.isRequired,
-  unsavedChanges: PropTypes.bool.isRequired,
+  showBuilderPane: PropTypes.bool.isRequired,
+  toggleBuilderPane: PropTypes.func.isRequired,
+  hasUnsavedChanges: PropTypes.bool.isRequired,
+
+  // redux
+  onUndo: PropTypes.func.isRequired,
+  onRedo: PropTypes.func.isRequired,
+  canUndo: PropTypes.bool.isRequired,
+  canRedo: PropTypes.bool.isRequired,
 };
 
 class Header extends React.PureComponent {
   constructor(props) {
     super(props);
-    this.handleSaveTitle = this.handleSaveTitle.bind(this);
+    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);
   }
-  handleSaveTitle(title) {
-    this.props.updateDashboardTitle(title);
+  handleChangeText(nextText) {
+    const { updateDashboardTitle, onChange } = this.props;
+    if (nextText && this.props.dashboardTitle !== nextText) {
+      updateDashboardTitle(nextText);
+      onChange();
+    }
   }
   toggleEditMode() {
     this.props.setEditMode(!this.props.editMode);
   }
   renderUnsaved() {
-    if (!this.props.unsavedChanges) {
+    if (!this.props.hasUnsavedChanges) {
       return null;
     }
     return (
@@ -54,66 +72,86 @@ class Header extends React.PureComponent {
       />
     );
   }
+  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.dashboard.dash_save_perm) {
+    if (!this.props.dashboardInfo.dash_save_perm) {
       return null;
     }
-    const btnText = this.props.editMode ? 'Switch to View Mode' : 'Edit Dashboard';
+    const btnText = this.props.editMode ? t('Switch to View Mode') : t('Edit Dashboard');
     return (
-      <Button
-        bsStyle="default"
-        className="m-r-5"
-        style={{ width: '150px' }}
-        onClick={this.toggleEditMode}
-      >
+      <Button bsSize="small" onClick={this.toggleEditMode}>
         {btnText}
-      </Button>);
+      </Button>
+    );
   }
   render() {
-    const dashboard = this.props.dashboard;
+    const {
+      dashboardTitle,
+      layout,
+      filters,
+      expandedSlices,
+      onUndo,
+      onRedo,
+      canUndo,
+      canRedo,
+      onChange,
+      onSave,
+      editMode,
+    } = this.props;
+
     return (
-      <div className="title">
-        <div className="pull-left">
-          <h1 className="outer-container pull-left">
-            <EditableTitle
-              title={dashboard.dashboard_title}
-              canEdit={dashboard.dash_save_perm && this.props.editMode}
-              onSaveTitle={this.handleSaveTitle}
-              showTooltip={this.props.editMode}
-            />
-            <span className="favstar m-r-5">
-              <FaveStar
-                itemId={dashboard.id}
-                fetchFaveStar={this.props.fetchFaveStar}
-                saveFaveStar={this.props.saveFaveStar}
-                isStarred={this.props.isStarred}
-              />
-            </span>
-            {this.renderUnsaved()}
-          </h1>
-        </div>
-        <div className="pull-right" style={{ marginTop: '35px' }}>
-          <span className="m-r-5">
-            <URLShortLinkButton
-              emailSubject="Superset Dashboard"
-              emailContent="Check out this dashboard: "
+      <div className="dashboard-header">
+        <div className="dashboard-component-header header-large">
+          <EditableTitle
+            title={dashboardTitle}
+            canEdit={this.props.dashboardInfo.dash_save_perm && editMode}
+            onSaveTitle={this.handleChangeText}
+            showTooltip={editMode}
+          />
+          <span className="favstar m-r-5">
+            <FaveStar
+              itemId={this.props.dashboardInfo.id}
+              fetchFaveStar={this.props.fetchFaveStar}
+              saveFaveStar={this.props.saveFaveStar}
+              isStarred={this.props.isStarred}
             />
           </span>
-          {this.renderEditButton()}
+          {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>
           <Controls
-            dashboard={dashboard}
-            filters={this.props.filters}
-            userId={this.props.userId}
-            addSlicesToDashboard={this.props.addSlicesToDashboard}
-            onSave={this.props.onSave}
-            onChange={this.props.onChange}
-            renderSlices={this.props.renderSlices}
-            serialize={this.props.serialize}
+            dashboardInfo={this.props.dashboardInfo}
+            dashboardTitle={dashboardTitle}
+            layout={layout}
+            filters={filters}
+            expandedSlices={expandedSlices}
+            onSave={onSave}
+            onChange={onChange}
+            forceRefreshAllCharts={this.forceRefresh}
             startPeriodicRender={this.props.startPeriodicRender}
-            editMode={this.props.editMode}
+            editMode={editMode}
           />
-        </div>
-        <div className="clearfix" />
+        </ButtonToolbar>
       </div>
     );
   }
diff --git a/superset/assets/src/dashboard/components/RefreshIntervalModal.jsx b/superset/assets/src/dashboard/components/RefreshIntervalModal.jsx
index 93c4272..c2a5637 100644
--- a/superset/assets/src/dashboard/components/RefreshIntervalModal.jsx
+++ b/superset/assets/src/dashboard/components/RefreshIntervalModal.jsx
@@ -48,8 +48,11 @@ class RefreshIntervalModal extends React.PureComponent {
               options={options}
               value={this.state.refreshFrequency}
               onChange={(opt) => {
-                this.setState({ refreshFrequency: opt.value });
-                this.props.onChange(opt.value);
+                const value = opt ? opt.value : options[0].value;
+                this.setState({
+                  refreshFrequency: value,
+                });
+                this.props.onChange(value);
               }}
             />
           </div>
diff --git a/superset/assets/src/dashboard/components/SaveModal.jsx b/superset/assets/src/dashboard/components/SaveModal.jsx
index d693385..2e76bf4 100644
--- a/superset/assets/src/dashboard/components/SaveModal.jsx
+++ b/superset/assets/src/dashboard/components/SaveModal.jsx
@@ -1,31 +1,30 @@
 /* global notify */
 import React from 'react';
 import PropTypes from 'prop-types';
+import $ from 'jquery';
+
 import { Button, FormControl, FormGroup, Radio } from 'react-bootstrap';
 import { getAjaxErrorMsg } from '../../modules/utils';
 import ModalTrigger from '../../components/ModalTrigger';
 import { t } from '../../locales';
 import Checkbox from '../../components/Checkbox';
 
-const $ = window.$ = require('jquery');
-
 const propTypes = {
-  css: PropTypes.string,
-  dashboard: PropTypes.object.isRequired,
+  dashboardId: PropTypes.number.isRequired,
+  dashboardTitle: PropTypes.string.isRequired,
+  expandedSlices: PropTypes.object.isRequired,
+  layout: PropTypes.object.isRequired,
   triggerNode: PropTypes.node.isRequired,
   filters: PropTypes.object.isRequired,
-  serialize: PropTypes.func,
-  onSave: PropTypes.func,
+  onSave: PropTypes.func.isRequired,
 };
 
 class SaveModal extends React.PureComponent {
   constructor(props) {
     super(props);
     this.state = {
-      dashboard: props.dashboard,
-      css: props.css,
       saveType: 'overwrite',
-      newDashName: props.dashboard.dashboard_title + ' [copy]',
+      newDashName: props.dashboardTitle + ' [copy]',
       duplicateSlices: false,
     };
     this.modal = null;
@@ -50,7 +49,6 @@ class SaveModal extends React.PureComponent {
   saveDashboardRequest(data, url, saveType) {
     const saveModal = this.modal;
     const onSaveDashboard = this.props.onSave;
-    Object.assign(data, { css: this.props.css });
     $.ajax({
       type: 'POST',
       url,
@@ -74,19 +72,17 @@ class SaveModal extends React.PureComponent {
     });
   }
   saveDashboard(saveType, newDashboardTitle) {
-    const dashboard = this.props.dashboard;
-    const positions = this.props.serialize();
+    const { dashboardTitle, layout: positions, expandedSlices, filters, dashboardId } = this.props;
     const data = {
       positions,
-      css: this.state.css,
-      expanded_slices: dashboard.metadata.expanded_slices || {},
-      dashboard_title: dashboard.dashboard_title,
-      default_filters: JSON.stringify(this.props.filters),
+      expanded_slices: expandedSlices,
+      dashboard_title: dashboardTitle,
+      default_filters: JSON.stringify(filters),
       duplicate_slices: this.state.duplicateSlices,
     };
     let url = null;
     if (saveType === 'overwrite') {
-      url = `/superset/save_dash/${dashboard.id}/`;
+      url = `/superset/save_dash/${dashboardId}/`;
       this.saveDashboardRequest(data, url, saveType);
     } else if (saveType === 'newDashboard') {
       if (!newDashboardTitle) {
@@ -97,7 +93,7 @@ class SaveModal extends React.PureComponent {
         });
       } else {
         data.dashboard_title = newDashboardTitle;
-        url = `/superset/copy_dash/${dashboard.id}/`;
+        url = `/superset/copy_dash/${dashboardId}/`;
         this.saveDashboardRequest(data, url, saveType);
       }
     }
@@ -116,7 +112,7 @@ class SaveModal extends React.PureComponent {
               onChange={this.handleSaveTypeChange}
               checked={this.state.saveType === 'overwrite'}
             >
-              {t('Overwrite Dashboard [%s]', this.props.dashboard.dashboard_title)}
+              {t('Overwrite Dashboard [%s]', this.props.dashboardTitle)}
             </Radio>
             <hr />
             <Radio
diff --git a/superset/assets/src/dashboard/components/SliceAdder.jsx b/superset/assets/src/dashboard/components/SliceAdder.jsx
index e99d00f..6477fc4 100644
--- a/superset/assets/src/dashboard/components/SliceAdder.jsx
+++ b/superset/assets/src/dashboard/components/SliceAdder.jsx
@@ -1,219 +1,214 @@
 import React from 'react';
-import $ from 'jquery';
 import PropTypes from 'prop-types';
-import { BootstrapTable, TableHeaderColumn } from 'react-bootstrap-table';
+import cx from 'classnames';
+import { DropdownButton, MenuItem } from 'react-bootstrap';
+import { List } from 'react-virtualized';
+import SearchInput, { createFilter } from 'react-search-input';
 
-import ModalTrigger from '../../components/ModalTrigger';
-import { t } from '../../locales';
-
-require('react-bootstrap-table/css/react-bootstrap-table.css');
+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';
 
 const propTypes = {
-  dashboard: PropTypes.object.isRequired,
-  triggerNode: PropTypes.node.isRequired,
+  fetchAllSlices: PropTypes.func.isRequired,
+  isLoading: PropTypes.bool.isRequired,
+  slices: PropTypes.objectOf(slicePropShape).isRequired,
+  lastUpdated: PropTypes.number.isRequired,
+  errorMessage: PropTypes.string,
   userId: PropTypes.string.isRequired,
-  addSlicesToDashboard: PropTypes.func,
+  selectedSliceIds: PropTypes.object,
+  editMode: PropTypes.bool,
+};
+
+const defaultProps = {
+  selectedSliceIds: new Set(),
+  editMode: false,
 };
 
+const KEYS_TO_FILTERS = ['slice_name', 'viz_type', 'datasource_name'];
+const KEYS_TO_SORT = [
+  { key: 'slice_name', label: 'Name' },
+  { key: 'viz_type', label: 'Visualization' },
+  { key: 'datasource_name', label: 'Datasource' },
+  { key: 'changed_on', label: 'Recent' },
+];
+
 class SliceAdder extends React.Component {
   constructor(props) {
     super(props);
     this.state = {
-      slices: [],
-      slicesLoaded: false,
-      selectionMap: {},
+      filteredSlices: [],
+      searchTerm: '',
+      sortBy: KEYS_TO_SORT.findIndex(item => (item.key === 'changed_on')),
     };
 
-    this.options = {
-      defaultSortOrder: 'desc',
-      defaultSortName: 'modified',
-      sizePerPage: 10,
-    };
+    this.rowRenderer = this.rowRenderer.bind(this);
+    this.searchUpdated = this.searchUpdated.bind(this);
+    this.handleKeyPress = this.handleKeyPress.bind(this);
+    this.handleSelect = this.handleSelect.bind(this);
+  }
 
-    this.addSlices = this.addSlices.bind(this);
-    this.toggleSlice = this.toggleSlice.bind(this);
+  componentDidMount() {
+    this.slicesRequest = this.props.fetchAllSlices(this.props.userId);
+  }
 
-    this.selectRowProp = {
-      mode: 'checkbox',
-      clickToSelect: true,
-      onSelect: this.toggleSlice,
-    };
+  componentWillReceiveProps(nextProps) {
+    if (nextProps.lastUpdated !== this.props.lastUpdated) {
+      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)),
+      });
+    }
   }
 
   componentWillUnmount() {
-    if (this.slicesRequest) {
+    if (this.slicesRequest && this.slicesRequest.abort) {
       this.slicesRequest.abort();
     }
   }
 
-  onEnterModal() {
-    const uri = `/sliceaddview/api/read?_flt_0_created_by=${this.props.userId}`;
-    this.slicesRequest = $.ajax({
-      url: uri,
-      type: 'GET',
-      success: (response) => {
-        // Prepare slice data for table
-        const slices = response.result.map(slice => ({
-          id: slice.id,
-          sliceName: slice.slice_name,
-          vizType: slice.viz_type,
-          datasourceLink: slice.datasource_link,
-          modified: slice.modified,
-        }));
-
-        this.setState({
-          slices,
-          selectionMap: {},
-          slicesLoaded: true,
-        });
-      },
-      error: (error) => {
-        this.errored = true;
-        this.setState({
-          errorMsg: t('Sorry, there was an error fetching charts to this dashboard: ') +
-          this.getAjaxErrorMsg(error),
-        });
-      },
-    });
+  getFilteredSortedSlices(searchTerm, sortBy) {
+    return Object.values(this.props.slices)
+      .filter(createFilter(searchTerm, KEYS_TO_FILTERS))
+      .sort(this.sortByComparator(KEYS_TO_SORT[sortBy].key));
   }
 
-  getAjaxErrorMsg(error) {
-    const respJSON = error.responseJSON;
-    return (respJSON && respJSON.message) ? respJSON.message :
-      error.responseText;
+  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;
+    };
   }
 
-  addSlices() {
-    const adder = this;
-    this.props.addSlicesToDashboard(Object.keys(this.state.selectionMap))
-      // if successful, page will be reloaded.
-      .fail((error) => {
-        adder.errored = true;
-        adder.setState({
-          errorMsg: t('Sorry, there was an error adding charts to this dashboard: ') +
-          this.getAjaxErrorMsg(error),
-        });
-      });
+  handleKeyPress(ev) {
+    if (ev.key === 'Enter') {
+      ev.preventDefault();
+
+      this.searchUpdated(ev.target.value);
+    }
   }
 
-  toggleSlice(slice) {
-    const selectionMap = Object.assign({}, this.state.selectionMap);
-    selectionMap[slice.id] = !selectionMap[slice.id];
-    this.setState({ selectionMap });
+  searchUpdated(searchTerm) {
+    this.setState({
+      searchTerm,
+      filteredSlices: this.getFilteredSortedSlices(searchTerm, this.state.sortBy),
+    });
   }
 
-  modifiedDateComparator(a, b, order) {
-    if (order === 'desc') {
-      if (a.changed_on > b.changed_on) {
-        return -1;
-      } else if (a.changed_on < b.changed_on) {
-        return 1;
-      }
-      return 0;
-    }
+  handleSelect(sortBy) {
+    this.setState({
+      sortBy,
+      filteredSlices: this.getFilteredSortedSlices(this.state.searchTerm, sortBy),
+    });
+  }
 
-    if (a.changed_on < b.changed_on) {
-      return -1;
-    } else if (a.changed_on > b.changed_on) {
-      return 1;
-    }
-    return 0;
+  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;
+    const meta = {
+      chartId: cellData.slice_id,
+    };
+
+    return (
+      <DragDroppable
+        component={{ type, id, meta }}
+        parentComponent={{ id: NEW_COMPONENTS_SOURCE_ID, type: NEW_COMPONENT_SOURCE_TYPE }}
+        index={0}
+        depth={0}
+        disableDragDrop={isSelected}
+        editMode={this.props.editMode}
+      >
+        {({ dragSourceRef }) => (
+          <div
+            ref={dragSourceRef}
+            className="chart-card-container"
+            key={key}
+            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>
+        )}
+      </DragDroppable>
+    );
   }
 
   render() {
-    const hideLoad = this.state.slicesLoaded || this.errored;
-    let enableAddSlice = this.state.selectionMap && Object.keys(this.state.selectionMap);
-    if (enableAddSlice) {
-      enableAddSlice = enableAddSlice.some(function (key) {
-        return this.state.selectionMap[key];
-      }, this);
-    }
-    const modalContent = (
-      <div>
-        <img
-          src="/static/assets/images/loading.gif"
-          className={'loading ' + (hideLoad ? 'hidden' : '')}
-          alt={hideLoad ? '' : 'loading'}
-        />
-        <div className={this.errored ? '' : 'hidden'}>
-          {this.state.errorMsg}
-        </div>
-        <div className={this.state.slicesLoaded ? '' : 'hidden'}>
-          <BootstrapTable
-            ref="table"
-            data={this.state.slices}
-            selectRow={this.selectRowProp}
-            options={this.options}
-            hover
-            search
-            pagination
-            condensed
-            height="auto"
+    return (
+      <div className="slice-adder-container">
+        <div className="controls">
+          <DropdownButton
+            title={KEYS_TO_SORT[this.state.sortBy].label}
+            onSelect={this.handleSelect}
+            id="slice-adder-sortby"
           >
-            <TableHeaderColumn
-              dataField="id"
-              isKey
-              dataSort
-              hidden
+            {KEYS_TO_SORT.map((item, index) => (
+              <MenuItem key={item.key} eventKey={index}>{item.label}</MenuItem>
+            ))}
+          </DropdownButton>
+
+          <SearchInput
+            onChange={this.searchUpdated}
+            onKeyPress={this.handleKeyPress}
+          />
+        </div>
+
+        {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 &&
+            <List
+              width={376}
+              height={500}
+              rowCount={this.state.filteredSlices.length}
+              rowHeight={136}
+              rowRenderer={this.rowRenderer}
+              searchTerm={this.state.searchTerm}
+              sortBy={this.state.sortBy}
+              selectedSliceIds={this.props.selectedSliceIds}
             />
-            <TableHeaderColumn
-              dataField="sliceName"
-              dataSort
-            >
-              {t('Name')}
-            </TableHeaderColumn>
-            <TableHeaderColumn
-              dataField="vizType"
-              dataSort
-            >
-              {t('Viz')}
-            </TableHeaderColumn>
-            <TableHeaderColumn
-              dataField="datasourceLink"
-              dataSort
-              // Will cause react-bootstrap-table to interpret the HTML returned
-              dataFormat={datasourceLink => datasourceLink}
-            >
-              {t('Datasource')}
-            </TableHeaderColumn>
-            <TableHeaderColumn
-              dataField="modified"
-              dataSort
-              sortFunc={this.modifiedDateComparator}
-              // Will cause react-bootstrap-table to interpret the HTML returned
-              dataFormat={modified => modified}
-            >
-              {t('Modified')}
-            </TableHeaderColumn>
-          </BootstrapTable>
-          <button
-            type="button"
-            className="btn btn-default"
-            data-dismiss="modal"
-            onClick={this.addSlices}
-            disabled={!enableAddSlice}
-          >
-            {t('Add Charts')}
-          </button>
+          }
         </div>
       </div>
     );
-
-    return (
-      <ModalTrigger
-        triggerNode={this.props.triggerNode}
-        tooltip={t('Add a new chart to the dashboard')}
-        beforeOpen={this.onEnterModal.bind(this)}
-        isMenuItem
-        modalBody={modalContent}
-        bsSize="large"
-        setModalAsTriggerChildren
-        modalTitle={t('Add Charts to Dashboard')}
-      />
-    );
   }
 }
 
 SliceAdder.propTypes = propTypes;
+SliceAdder.defaultProps = defaultProps;
 
 export default SliceAdder;
diff --git a/superset/assets/src/dashboard/components/SliceAdderContainer.jsx b/superset/assets/src/dashboard/components/SliceAdderContainer.jsx
new file mode 100644
index 0000000..b4f10d9
--- /dev/null
+++ b/superset/assets/src/dashboard/components/SliceAdderContainer.jsx
@@ -0,0 +1,25 @@
+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 6db9c68..f126949 100644
--- a/superset/assets/src/dashboard/components/SliceHeader.jsx
+++ b/superset/assets/src/dashboard/components/SliceHeader.jsx
@@ -1,11 +1,10 @@
 import React from 'react';
 import PropTypes from 'prop-types';
-import moment from 'moment';
-import { connect } from 'react-redux';
 
 import { t } from '../../locales';
 import EditableTitle from '../../components/EditableTitle';
 import TooltipWrapper from '../../components/TooltipWrapper';
+import SliceHeaderControls from './SliceHeaderControls';
 
 const propTypes = {
   slice: PropTypes.object.isRequired,
@@ -14,7 +13,6 @@ const propTypes = {
   isExpanded: PropTypes.bool,
   isCached: PropTypes.bool,
   cachedDttm: PropTypes.string,
-  removeSlice: PropTypes.func,
   updateSliceName: PropTypes.func,
   toggleExpandSlice: PropTypes.func,
   forceRefresh: PropTypes.func,
@@ -40,11 +38,6 @@ class SliceHeader extends React.PureComponent {
     super(props);
 
     this.onSaveTitle = this.onSaveTitle.bind(this);
-    this.onToggleExpandSlice = this.onToggleExpandSlice.bind(this);
-    this.exportCSV = this.props.exportCSV.bind(this, this.props.slice);
-    this.exploreChart = this.props.exploreChart.bind(this, this.props.slice);
-    this.forceRefresh = this.props.forceRefresh.bind(this, this.props.slice.slice_id);
-    this.removeSlice = this.props.removeSlice.bind(this, this.props.slice);
   }
 
   onSaveTitle(newTitle) {
@@ -53,17 +46,12 @@ class SliceHeader extends React.PureComponent {
     }
   }
 
-  onToggleExpandSlice() {
-    this.props.toggleExpandSlice(this.props.slice, !this.props.isExpanded);
-  }
-
   render() {
-    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 {
+      slice, isExpanded, isCached, cachedDttm,
+      toggleExpandSlice, forceRefresh,
+      exploreChart, exportCSV,
+    } = this.props;
     const annoationsLoading = t('Annotation layers are still loading.');
     const annoationsError = t('One ore more annotation layers failed loading.');
 
@@ -96,83 +84,18 @@ class SliceHeader extends React.PureComponent {
                 <i className="fa fa-exclamation-circle danger" />
               </TooltipWrapper>
             }
-          </div>
-          <div className="chart-controls">
-            <div id={'controls_' + slice.slice_id} className="pull-right">
-              {this.props.editMode &&
-                <a>
-                  <TooltipWrapper
-                    placement="top"
-                    label="move"
-                    tooltip={t('Move chart')}
-                  >
-                    <i className="fa fa-arrows drag" />
-                  </TooltipWrapper>
-                </a>
-              }
-              <a className={`refresh ${isCached ? 'danger' : ''}`} onClick={this.forceRefresh}>
-                <TooltipWrapper
-                  placement="top"
-                  label="refresh"
-                  tooltip={refreshTooltip}
-                >
-                  <i className="fa fa-repeat" />
-                </TooltipWrapper>
-              </a>
-              {slice.description &&
-              <a onClick={this.onToggleExpandSlice}>
-                <TooltipWrapper
-                  placement="top"
-                  label="description"
-                  tooltip={t('Toggle chart description')}
-                >
-                  <i className="fa fa-info-circle slice_info" />
-                </TooltipWrapper>
-              </a>
-              }
-              {this.props.sliceCanEdit &&
-                <a href={slice.edit_url} target="_blank">
-                  <TooltipWrapper
-                    placement="top"
-                    label="edit"
-                    tooltip={t('Edit chart')}
-                  >
-                    <i className="fa fa-pencil" />
-                  </TooltipWrapper>
-                </a>
-              }
-              <a className="exportCSV" onClick={this.exportCSV}>
-                <TooltipWrapper
-                  placement="top"
-                  label="exportCSV"
-                  tooltip={t('Export CSV')}
-                >
-                  <i className="fa fa-table" />
-                </TooltipWrapper>
-              </a>
-              {this.props.supersetCanExplore &&
-                <a className="exploreChart" onClick={this.exploreChart}>
-                  <TooltipWrapper
-                    placement="top"
-                    label="exploreChart"
-                    tooltip={t('Explore chart')}
-                  >
-                    <i className="fa fa-share" />
-                  </TooltipWrapper>
-                </a>
-              }
-              {this.props.editMode &&
-                <a className="remove-chart" onClick={this.removeSlice}>
-                  <TooltipWrapper
-                    placement="top"
-                    label="close"
-                    tooltip={t('Remove chart from dashboard')}
-                  >
-                    <i className="fa fa-close" />
-                  </TooltipWrapper>
-                </a>
-              }
-            </div>
+            {!this.props.editMode &&
+              <SliceHeaderControls
+                slice={slice}
+                isCached={isCached}
+                isExpanded={isExpanded}
+                cachedDttm={cachedDttm}
+                toggleExpandSlice={toggleExpandSlice}
+                forceRefresh={forceRefresh}
+                exploreChart={exploreChart}
+                exportCSV={exportCSV}
+              />
+            }
           </div>
         </div>
       </div>
@@ -183,12 +106,4 @@ class SliceHeader extends React.PureComponent {
 SliceHeader.propTypes = propTypes;
 SliceHeader.defaultProps = defaultProps;
 
-function mapStateToProps({ dashboard }) {
-  return {
-    supersetCanExplore: dashboard.dashboard.superset_can_explore,
-    sliceCanEdit: dashboard.dashboard.slice_can_edit,
-  };
-}
-
-export { SliceHeader };
-export default connect(mapStateToProps, () => ({}))(SliceHeader);
+export default SliceHeader;
diff --git a/superset/assets/src/dashboard/components/SliceHeaderControls.jsx b/superset/assets/src/dashboard/components/SliceHeaderControls.jsx
new file mode 100644
index 0000000..f61e59b
--- /dev/null
+++ b/superset/assets/src/dashboard/components/SliceHeaderControls.jsx
@@ -0,0 +1,106 @@
+import React from 'react';
+import PropTypes from 'prop-types';
+import cx from 'classnames';
+import moment from 'moment';
+import { DropdownButton } from 'react-bootstrap';
+
+import { ActionMenuItem } from './ActionMenuItem';
+import { t } from '../../locales';
+
+const propTypes = {
+  slice: PropTypes.object.isRequired,
+  isCached: PropTypes.bool,
+  isExpanded: PropTypes.bool,
+  cachedDttm: PropTypes.string,
+  toggleExpandSlice: PropTypes.func,
+  forceRefresh: PropTypes.func,
+  exploreChart: PropTypes.func,
+  exportCSV: PropTypes.func,
+};
+
+const defaultProps = {
+  forceRefresh: () => ({}),
+  toggleExpandSlice: () => ({}),
+  exploreChart: () => ({}),
+  exportCSV: () => ({}),
+};
+
+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.toggleControls = this.toggleControls.bind(this);
+
+    this.state = {
+      showControls: false,
+    };
+  }
+
+  toggleControls() {
+    this.setState({
+      showControls: !this.state.showControls,
+    });
+  }
+
+  render() {
+    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');
+
+    // @TODO account for
+    //  dashboard.dashboard.superset_can_explore
+    //  dashboard.dashboard.slice_can_edit
+    return (
+      <DropdownButton
+        title=""
+        id={`slice_${slice.slice_id}-controls`}
+        className={cx('slice-header-controls-trigger', 'fa fa-ellipsis-v', { 'is-cached': isCached })}
+        pullRight
+        noCaret
+      >
+        <ActionMenuItem
+          text={t('Force refresh data')}
+          tooltip={refreshTooltip}
+          onClick={this.props.forceRefresh}
+        />
+
+        {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')}
+          href={slice.edit_url}
+          target="_blank"
+        />
+
+        <ActionMenuItem
+          text={t('Export CSV')}
+          tooltip={t('Export CSV')}
+          onClick={this.exportCSV}
+        />
+
+        <ActionMenuItem
+          text={t('Explore chart')}
+          tooltip={t('Explore chart')}
+          onClick={this.exploreChart}
+        />
+      </DropdownButton>
+    );
+  }
+}
+
+SliceHeaderControls.propTypes = propTypes;
+SliceHeaderControls.defaultProps = defaultProps;
+
+export default SliceHeaderControls;
diff --git a/superset/assets/src/dashboard/index.jsx b/superset/assets/src/dashboard/index.jsx
index 1aadc58..9c00f9e 100644
--- a/superset/assets/src/dashboard/index.jsx
+++ b/superset/assets/src/dashboard/index.jsx
@@ -8,36 +8,18 @@ import { initEnhancer } from '../reduxUtils';
 import { appSetup } from '../common';
 import { initJQueryAjax } from '../modules/utils';
 import DashboardContainer from './components/DashboardContainer';
-// import rootReducer, { getInitialState } from './reducers';
-
-import emptyDashboardLayout from './v2/fixtures/emptyDashboardLayout';
-import rootReducer from './v2/reducers/';
+import getInitialState from './reducers/getInitialState';
+import rootReducer from './reducers/index';
 
 appSetup();
 initJQueryAjax();
 
 const appContainer = document.getElementById('app');
-// const bootstrapData = JSON.parse(appContainer.getAttribute('data-bootstrap'));
-// const initState = Object.assign({}, getInitialState(bootstrapData));
-
-const initState = {
-  dashboardLayout: {
-    past: [],
-    present: emptyDashboardLayout,
-    future: [],
-  },
-  editMode: true,
-  messageToasts: [],
-};
+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/reducers.js b/superset/assets/src/dashboard/reducers.js
deleted file mode 100644
index 01e6dc2..0000000
--- a/superset/assets/src/dashboard/reducers.js
+++ /dev/null
@@ -1,214 +0,0 @@
-/* eslint-disable camelcase */
-import { combineReducers } from 'redux';
-import d3 from 'd3';
-import shortid from 'shortid';
-
-import charts, { chart } from '../chart/chartReducer';
-import * as actions from './actions';
-import { getParam } from '../modules/utils';
-import { alterInArr, removeFromArr } from '../reduxUtils';
-import { applyDefaultFormData } from '../explore/store';
-import { getColorFromScheme } from '../modules/colors';
-
-export function getInitialState(bootstrapData) {
-  const { user_id, datasources, common, editMode } = bootstrapData;
-  delete common.locale;
-  delete common.language_pack;
-
-  const dashboard = { ...bootstrapData.dashboard_data };
-  let filters = {};
-  try {
-    // allow request parameter overwrite dashboard metadata
-    filters = JSON.parse(getParam('preselect_filters') || dashboard.metadata.default_filters);
-  } catch (e) {
-    //
-  }
-
-  // Priming the color palette with user's label-color mapping provided in
-  // the dashboard's JSON metadata
-  if (dashboard.metadata && dashboard.metadata.label_colors) {
-    const colorMap = dashboard.metadata.label_colors;
-    for (const label in colorMap) {
-      getColorFromScheme(label, null, colorMap[label]);
-    }
-  }
-
-  dashboard.posDict = {};
-  dashboard.layout = [];
-  if (Array.isArray(dashboard.position_json)) {
-    dashboard.position_json.forEach((position) => {
-      dashboard.posDict[position.slice_id] = position;
-    });
-  } else {
-    dashboard.position_json = [];
-  }
-
-  const lastRowId = Math.max(0, Math.max.apply(null,
-    dashboard.position_json.map(pos => (pos.row + pos.size_y))));
-  let newSliceCounter = 0;
-  dashboard.slices.forEach((slice) => {
-    const sliceId = slice.slice_id;
-    let pos = dashboard.posDict[sliceId];
-    if (!pos) {
-      // append new slices to dashboard bottom, 3 slices per row
-      pos = {
-        col: (newSliceCounter % 3) * 16 + 1,
-        row: lastRowId + Math.floor(newSliceCounter / 3) * 16,
-        size_x: 16,
-        size_y: 16,
-      };
-      newSliceCounter++;
-    }
-
-    dashboard.layout.push({
-      i: String(sliceId),
-      x: pos.col - 1,
-      y: pos.row,
-      w: pos.size_x,
-      minW: 2,
-      h: pos.size_y,
-    });
-  });
-
-  // will use charts action/reducers to handle chart render
-  const initCharts = {};
-  dashboard.slices.forEach((slice) => {
-    const chartKey = 'slice_' + slice.slice_id;
-    initCharts[chartKey] = { ...chart,
-      chartKey,
-      slice_id: slice.slice_id,
-      form_data: slice.form_data,
-      formData: applyDefaultFormData(slice.form_data),
-    };
-  });
-
-  // also need to add formData for dashboard.slices
-  dashboard.slices = dashboard.slices.map(slice =>
-    ({ ...slice, formData: applyDefaultFormData(slice.form_data) }),
-  );
-
-  return {
-    charts: initCharts,
-    dashboard: { filters, dashboard, userId: user_id, datasources, common, editMode },
-  };
-}
-
-export const dashboard = function (state = {}, action) {
-  const actionHandlers = {
-    [actions.UPDATE_DASHBOARD_TITLE]() {
-      const newDashboard = { ...state.dashboard, dashboard_title: action.title };
-      return { ...state, dashboard: newDashboard };
-    },
-    [actions.UPDATE_DASHBOARD_LAYOUT]() {
-      const newDashboard = { ...state.dashboard, layout: action.layout };
-      return { ...state, dashboard: newDashboard };
-    },
-    [actions.REMOVE_SLICE]() {
-      const key = String(action.slice.slice_id);
-      const newLayout = state.dashboard.layout.filter(reactPos => (reactPos.i !== key));
-      const newDashboard = removeFromArr(state.dashboard, 'slices', action.slice, 'slice_id');
-      // if this slice is a filter
-      const newFilter = { ...state.filters };
-      let refresh = false;
-      if (state.filters[key]) {
-        delete newFilter[key];
-        refresh = true;
-      }
-      return {
-        ...state,
-        dashboard: { ...newDashboard, layout: newLayout },
-        filters: newFilter,
-        refresh,
-      };
-    },
-    [actions.TOGGLE_FAVE_STAR]() {
-      return { ...state, isStarred: action.isStarred };
-    },
-    [actions.SET_EDIT_MODE]() {
-      return { ...state, editMode: action.editMode };
-    },
-    [actions.TOGGLE_EXPAND_SLICE]() {
-      const updatedExpandedSlices = { ...state.dashboard.metadata.expanded_slices };
-      const sliceId = action.slice.slice_id;
-      if (action.isExpanded) {
-        updatedExpandedSlices[sliceId] = true;
-      } else {
-        delete updatedExpandedSlices[sliceId];
-      }
-      const metadata = { ...state.dashboard.metadata, expanded_slices: updatedExpandedSlices };
-      const newDashboard = { ...state.dashboard, metadata };
-      return { ...state, dashboard: newDashboard };
-    },
-
-    // filters
-    [actions.ADD_FILTER]() {
-      const selectedSlice = state.dashboard.slices
-        .find(slice => (slice.slice_id === action.sliceId));
-      if (!selectedSlice) {
-        return state;
-      }
-
-      let filters = state.filters;
-      const { sliceId, col, vals, merge, refresh } = action;
-      const filterKeys = ['__from', '__to', '__time_col',
-        '__time_grain', '__time_origin', '__granularity'];
-      if (filterKeys.indexOf(col) >= 0 ||
-        selectedSlice.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) {
-          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
-        } else if (filters[sliceId][col] instanceof Array) {
-          newFilter[col] = d3.merge([filters[sliceId][col], vals]);
-        } else {
-          newFilter[col] = d3.merge([[filters[sliceId][col]], vals])[0] || '';
-        }
-        filters = { ...filters, [sliceId]: newFilter };
-      }
-      return { ...state, filters, refresh };
-    },
-    [actions.CLEAR_FILTER]() {
-      const newFilters = { ...state.filters };
-      delete newFilters[action.sliceId];
-      return { ...state, filter: newFilters, refresh: true };
-    },
-    [actions.REMOVE_FILTER]() {
-      const { sliceId, col, vals, refresh } = action;
-      const excluded = new Set(vals);
-      const valFilter = val => !excluded.has(val);
-
-      let filters = state.filters;
-      // Have to be careful not to modify the dashboard state so that
-      // the render actually triggers
-      if (sliceId in state.filters && col in state.filters[sliceId]) {
-        const newFilter = filters[sliceId][col].filter(valFilter);
-        filters = { ...filters, [sliceId]: newFilter };
-      }
-      return { ...state, filters, refresh };
-    },
-
-    // slice reducer
-    [actions.UPDATE_SLICE_NAME]() {
-      const newDashboard = alterInArr(
-        state.dashboard, 'slices',
-        action.slice, { slice_name: action.sliceName },
-        'slice_id');
-      return { ...state, dashboard: newDashboard };
-    },
-  };
-
-  if (action.type in actionHandlers) {
-    return actionHandlers[action.type]();
-  }
-  return state;
-};
-
-export default combineReducers({
-  charts,
-  dashboard,
-  impressionId: () => (shortid.generate()),
-});
diff --git a/superset/assets/src/dashboard/reducers/dashboardState.js b/superset/assets/src/dashboard/reducers/dashboardState.js
new file mode 100644
index 0000000..84ee58e
--- /dev/null
+++ b/superset/assets/src/dashboard/reducers/dashboardState.js
@@ -0,0 +1,128 @@
+/* eslint-disable camelcase */
+import { merge as mergeArray } from 'd3';
+
+import {
+  ADD_SLICE,
+  ADD_FILTER,
+  ON_CHANGE,
+  ON_SAVE,
+  REMOVE_SLICE,
+  REMOVE_FILTER,
+  SET_EDIT_MODE,
+  TOGGLE_BUILDER_PANE,
+  TOGGLE_EXPAND_SLICE,
+  TOGGLE_FAVE_STAR,
+  UPDATE_DASHBOARD_TITLE,
+} from '../actions/dashboardState';
+
+export default function (state = {}, action) {
+  const actionHandlers = {
+    [UPDATE_DASHBOARD_TITLE]() {
+      return { ...state, title: action.title };
+    },
+    [ADD_SLICE]() {
+      const updatedSliceIds = new Set(state.sliceIds);
+      updatedSliceIds.add(action.slice.slice_id);
+      return {
+        ...state,
+        sliceIds: updatedSliceIds,
+      };
+    },
+    [REMOVE_SLICE]() {
+      const sliceId = action.sliceId;
+      const updatedSliceIds = new Set(state.sliceIds);
+      updatedSliceIds.delete(sliceId);
+
+      const key = sliceId;
+      // if this slice is a filter
+      const newFilter = { ...state.filters };
+      let refresh = false;
+      if (state.filters[key]) {
+        delete newFilter[key];
+        refresh = true;
+      }
+      return {
+        ...state,
+        sliceIds: updatedSliceIds,
+        filters: newFilter,
+        refresh,
+      };
+    },
+    [TOGGLE_FAVE_STAR]() {
+      return { ...state, isStarred: action.isStarred };
+    },
+    [SET_EDIT_MODE]() {
+      return { ...state, editMode: action.editMode };
+    },
+    [TOGGLE_BUILDER_PANE]() {
+      return { ...state, showBuilderPane: !state.showBuilderPane };
+    },
+    [TOGGLE_EXPAND_SLICE]() {
+      const updatedExpandedSlices = { ...state.expandedSlices };
+      const sliceId = action.sliceId;
+      if (updatedExpandedSlices[sliceId]) {
+        delete updatedExpandedSlices[sliceId];
+      } else {
+        updatedExpandedSlices[sliceId] = true;
+      }
+      return { ...state, expandedSlices: updatedExpandedSlices };
+    },
+    [ON_CHANGE]() {
+      return { ...state, hasUnsavedChanges: true };
+    },
+    [ON_SAVE]() {
+      return { ...state, hasUnsavedChanges: false };
+    },
+
+    // filters
+    [ADD_FILTER]() {
+      const hasSelectedFilter = state.sliceIds.has(action.chart.id);
+      if (!hasSelectedFilter) {
+        return state;
+      }
+
+      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) {
+        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) {
+          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
+        } else if (filters[sliceId][col] instanceof Array) {
+          newFilter[col] = mergeArray([filters[sliceId][col], vals]);
+        } else {
+          newFilter[col] = mergeArray([[filters[sliceId][col]], vals])[0] || '';
+        }
+        filters = { ...filters, [sliceId]: newFilter };
+      }
+      return { ...state, filters, refresh };
+    },
+    [REMOVE_FILTER]() {
+      const { sliceId, col, vals, refresh } = action;
+      const excluded = new Set(vals);
+      const valFilter = val => !excluded.has(val);
+
+      let filters = state.filters;
+      // Have to be careful not to modify the dashboard state so that
+      // the render actually triggers
+      if (sliceId in state.filters && col in state.filters[sliceId]) {
+        const newFilter = filters[sliceId][col].filter(valFilter);
+        filters = { ...filters, [sliceId]: newFilter };
+      }
+      return { ...state, filters, refresh };
+    },
+  };
+
+  if (action.type in actionHandlers) {
+    return actionHandlers[action.type]();
+  }
+  return state;
+}
diff --git a/superset/assets/src/dashboard/reducers/datasources.js b/superset/assets/src/dashboard/reducers/datasources.js
new file mode 100644
index 0000000..4df7507
--- /dev/null
+++ b/superset/assets/src/dashboard/reducers/datasources.js
@@ -0,0 +1,17 @@
+import * as actions from '../actions/datasources';
+
+export default function datasourceReducer(datasources = {}, action) {
+  const actionHandlers = {
+    [actions.SET_DATASOURCE]() {
+      return action.datasource;
+    },
+  };
+
+  if (action.type in actionHandlers) {
+    return {
+      ...datasources,
+      [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
new file mode 100644
index 0000000..1129210
--- /dev/null
+++ b/superset/assets/src/dashboard/reducers/getInitialState.js
@@ -0,0 +1,109 @@
+/* eslint-disable camelcase */
+import shortid from 'shortid';
+
+import { chart } from '../../chart/chartReducer';
+import { initSliceEntities } from './sliceEntities';
+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';
+
+export default function (bootstrapData) {
+  const { user_id, datasources, common } = bootstrapData;
+  delete common.locale;
+  delete common.language_pack;
+
+  const dashboard = { ...bootstrapData.dashboard_data };
+  let filters = {};
+  try {
+    // allow request parameter overwrite dashboard metadata
+    filters = JSON.parse(getParam('preselect_filters') || dashboard.metadata.default_filters);
+  } catch (e) {
+    //
+  }
+
+  // Priming the color palette with user's label-color mapping provided in
+  // the dashboard's JSON metadata
+  if (dashboard.metadata && dashboard.metadata.label_colors) {
+    const colorMap = dashboard.metadata.label_colors;
+    for (const label in colorMap) {
+      getColorFromScheme(label, null, colorMap[label]);
+    }
+  }
+
+  // dashboard layout
+  const positionJson = dashboard.position_json;
+  let layout;
+  if (!positionJson || !positionJson[DASHBOARD_ROOT_ID]) {
+    layout = layoutConverter(dashboard);
+  } else {
+    layout = positionJson;
+  }
+
+  const dashboardLayout = {
+    past: [],
+    present: layout,
+    future: [],
+  };
+  delete dashboard.position_json;
+  delete dashboard.css;
+
+  const chartQueries = {};
+  const slices = {};
+  const sliceIds = new Set();
+  dashboard.slices.forEach((slice) => {
+    const key = slice.slice_id;
+    chartQueries[key] = { ...chart,
+      id: key,
+      form_data: slice.form_data,
+      formData: applyDefaultFormData(slice.form_data),
+    };
+
+    slices[key] = {
+      slice_id: key,
+      slice_url: slice.slice_url,
+      slice_name: slice.slice_name,
+      form_data: slice.form_data,
+      edit_url: slice.edit_url,
+      viz_type: slice.form_data.viz_type,
+      datasource: slice.form_data.datasource,
+      description: slice.description,
+      description_markeddown: slice.description_markeddown,
+    };
+
+    sliceIds.add(key);
+  });
+
+  return {
+    datasources,
+    sliceEntities: { ...initSliceEntities, slices, isLoading: false },
+    charts: chartQueries,
+    dashboardInfo: {  /* readOnly props */
+      id: dashboard.id,
+      slug: dashboard.slug,
+      metadata: {
+        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,
+      },
+      userId: user_id,
+      dash_edit_perm: dashboard.dash_edit_perm,
+      dash_save_perm: dashboard.dash_save_perm,
+      common,
+    },
+    dashboardState: {
+      title: dashboard.dashboard_title,
+      sliceIds,
+      refresh: false,
+      filters,
+      expandedSlices: dashboard.metadata.expanded_slices || {},
+      editMode: false,
+      showBuilderPane: false,
+      hasUnsavedChanges: false,
+    },
+    dashboardLayout,
+    messageToasts: [],
+    impressionId: shortid.generate(),
+  };
+}
diff --git a/superset/assets/src/dashboard/reducers/index.js b/superset/assets/src/dashboard/reducers/index.js
new file mode 100644
index 0000000..a2397e0
--- /dev/null
+++ b/superset/assets/src/dashboard/reducers/index.js
@@ -0,0 +1,22 @@
+import { combineReducers } from 'redux';
+
+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';
+
+const dashboardInfo = (state = {}) => (state);
+const impressionId = (state = '') => (state);
+
+export default combineReducers({
+  charts,
+  datasources,
+  sliceEntities,
+  dashboardInfo,
+  dashboardState,
+  dashboardLayout,
+  messageToasts,
+  impressionId,
+});
diff --git a/superset/assets/src/dashboard/reducers/sliceEntities.js b/superset/assets/src/dashboard/reducers/sliceEntities.js
new file mode 100644
index 0000000..61a58f6
--- /dev/null
+++ b/superset/assets/src/dashboard/reducers/sliceEntities.js
@@ -0,0 +1,62 @@
+import {
+  FETCH_ALL_SLICES_FAILED,
+  FETCH_ALL_SLICES_STARTED,
+  SET_ALL_SLICES,
+  UPDATE_SLICE_NAME,
+} from '../actions/sliceEntities';
+import { t } from '../../locales';
+
+export const initSliceEntities = {
+  slices: {},
+  isLoading: true,
+  errorMessage: null,
+  lastUpdated: 0,
+};
+
+export default function (state = initSliceEntities, action) {
+  const actionHandlers = {
+    [UPDATE_SLICE_NAME]() {
+      const updatedSlice = {
+        ...state.slices[action.key],
+        slice_name: action.sliceName,
+      };
+      const updatedSlices = {
+        ...state.slices,
+        [action.key]: updatedSlice,
+      };
+      return { ...state, slices: updatedSlices };
+    },
+    [FETCH_ALL_SLICES_STARTED]() {
+      return {
+        ...state,
+        isLoading: true,
+      };
+    },
+    [SET_ALL_SLICES]() {
+      return {
+        ...state,
+        isLoading: false,
+        slices: { ...state.slices, ...action.slices }, // append more slices
+        lastUpdated: new Date().getTime(),
+      };
+    },
+    [FETCH_ALL_SLICES_FAILED]() {
+      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;
+      return {
+        ...state,
+        isLoading: false,
+        errorMessage,
+        lastUpdated: new Date().getTime(),
+      };
+    },
+  };
+
+  if (action.type in actionHandlers) {
+    return actionHandlers[action.type]();
+  }
+  return state;
+}
diff --git a/superset/assets/src/dashboard/util/dashboardHelper.js b/superset/assets/src/dashboard/util/dashboardHelper.js
new file mode 100644
index 0000000..c9a6021
--- /dev/null
+++ b/superset/assets/src/dashboard/util/dashboardHelper.js
@@ -0,0 +1,9 @@
+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
new file mode 100644
index 0000000..854ca65
--- /dev/null
+++ b/superset/assets/src/dashboard/util/dashboardLayoutConverter.js
@@ -0,0 +1,322 @@
+/* eslint-disable no-param-reassign */
+/* eslint-disable camelcase */
+/* eslint-disable no-loop-func */
+import {
+  ROW_TYPE,
+  COLUMN_TYPE,
+  CHART_TYPE,
+  DASHBOARD_HEADER_TYPE,
+  DASHBOARD_ROOT_TYPE,
+  DASHBOARD_GRID_TYPE,
+} from '../v2/util/componentTypes';
+import {
+  DASHBOARD_GRID_ID,
+  DASHBOARD_HEADER_ID,
+  DASHBOARD_ROOT_ID,
+} from '../v2/util/constants';
+
+const MAX_RECURSIVE_LEVEL = 6;
+const GRID_RATIO = 4;
+const ROW_HEIGHT = 8;
+const generateId = (() => {
+  let componentId = 1;
+  return () => (componentId++);
+})();
+
+/**
+ *
+ * @param positions: single array of slices
+ * @returns boundary object {top: number, bottom: number, left: number, right: number}
+ */
+function getBoundary(positions) {
+  let top = Number.MAX_VALUE;
+  let bottom = 0;
+  let left = Number.MAX_VALUE;
+  let right = 1;
+  positions.forEach((item) => {
+    const { row, col, size_x, size_y } = item;
+    if (row <= top) top = row;
+    if (col <= left) left = col;
+    if (bottom <= row + size_y) bottom = row + size_y;
+    if (right <= col + size_x) right = col + size_x;
+  });
+
+  return {
+    top,
+    bottom,
+    left,
+    right,
+  };
+}
+
+function getRowContainer() {
+  const id = 'DASHBOARD_ROW_TYPE-' + generateId();
+  return {
+    version: 'v2',
+    type: ROW_TYPE,
+    id,
+    children: [],
+    meta: {
+      background: 'BACKGROUND_TRANSPARENT',
+    },
+  };
+}
+
+function getColContainer() {
+  const id = 'DASHBOARD_COLUMN_TYPE-' + generateId();
+  return {
+    version: 'v2',
+    type: COLUMN_TYPE,
+    id,
+    children: [],
+    meta: {
+      background: 'BACKGROUND_TRANSPARENT',
+    },
+  };
+}
+
+function getChartHolder(item) {
+  const { row, col, size_x, size_y, slice_id } = item;
+  const converted = {
+    row: Math.round(row / GRID_RATIO),
+    col: Math.floor((col - 1) / GRID_RATIO) + 1,
+    size_x: Math.max(1, Math.floor(size_x / GRID_RATIO)),
+    size_y: Math.max(1, Math.round(size_y / GRID_RATIO)),
+    slice_id,
+  };
+
+  return {
+    version: 'v2',
+    type: CHART_TYPE,
+    id: 'DASHBOARD_CHART_TYPE-' + generateId(),
+    children: [],
+    meta: {
+      width: converted.size_x,
+      height: Math.round(converted.size_y * 100 / ROW_HEIGHT),
+      chartId: slice_id,
+    },
+  };
+}
+
+function getChildrenMax(items, attr, layout) {
+  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);
+}
+
+function sortByRowId(item1, item2) {
+  return item1.row - item2.row;
+}
+
+function sortByColId(item1, item2) {
+  return item1.col - item2.col;
+}
+
+function hasOverlap(positions, xAxis = true) {
+  return positions.slice()
+    .sort(xAxis ? sortByColId : sortByRowId)
+    .some((item, index, arr) => {
+      if (index === arr.length - 1) {
+        return false;
+      }
+
+      if (xAxis) {
+        return (item.col + item.size_x) > arr[index + 1].col;
+      }
+      return (item.row + item.size_y) > arr[index + 1].row;
+    });
+}
+
+function doConvert(positions, level, parent, root) {
+  if (positions.length === 0) {
+    return;
+  }
+
+  if (positions.length === 1 || level >= MAX_RECURSIVE_LEVEL) {
+    // special treatment for single chart dash, always wrap chart inside a row
+    if (parent.type === 'DASHBOARD_GRID_TYPE') {
+      const rowContainer = getRowContainer();
+      root[rowContainer.id] = rowContainer;
+      parent.children.push(rowContainer.id);
+      parent = rowContainer;
+    }
+
+    const chartHolder = getChartHolder(positions[0]);
+    root[chartHolder.id] = chartHolder;
+    parent.children.push(chartHolder.id);
+    return;
+  }
+
+  let currentItems = positions.slice();
+  const { top, bottom, left, right } = getBoundary(positions);
+  // find row dividers
+  const layers = [];
+  let currentRow = top + 1;
+  while (currentItems.length && currentRow <= bottom) {
+    const upper = [];
+    const lower = [];
+
+    const isRowDivider = currentItems.every((item) => {
+      const { row, size_y } = item;
+      if (row + size_y <= currentRow) {
+        lower.push(item);
+        return true;
+      } else if (row >= currentRow) {
+        upper.push(item);
+        return true;
+      }
+      return false;
+    });
+
+    if (isRowDivider) {
+      currentItems = upper.slice();
+      layers.push(lower);
+    }
+    currentRow++;
+  }
+
+  layers.forEach((layer) => {
+    if (layer.length === 0) {
+      return;
+    }
+
+    if (layer.length === 1) {
+      const chartHolder = getChartHolder(layer[0]);
+      root[chartHolder.id] = chartHolder;
+      parent.children.push(chartHolder.id);
+      return;
+    }
+
+    // create a new row
+    const rowContainer = getRowContainer();
+    root[rowContainer.id] = rowContainer;
+    parent.children.push(rowContainer.id);
+
+    currentItems = layer.slice();
+    if (!hasOverlap(currentItems)) {
+      currentItems.sort(sortByColId).forEach((item) => {
+        const chartHolder = getChartHolder(item);
+        root[chartHolder.id] = chartHolder;
+        rowContainer.children.push(chartHolder.id);
+      });
+    } else {
+      // find col dividers for each layer
+      let currentCol = left + 1;
+      while (currentItems.length && currentCol <= right) {
+        const upper = [];
+        const lower = [];
+
+        const isColDivider = currentItems.every((item) => {
+          const { col, size_x } = item;
+          if (col + size_x <= currentCol) {
+            lower.push(item);
+            return true;
+          } else if (col >= currentCol) {
+            upper.push(item);
+            return true;
+          }
+          return false;
+        });
+
+        if (isColDivider) {
+          if (lower.length === 1) {
+            const chartHolder = getChartHolder(lower[0]);
+            root[chartHolder.id] = chartHolder;
+            rowContainer.children.push(chartHolder.id);
+          } else {
+            // create a new column
+            const colContainer = getColContainer();
+            root[colContainer.id] = colContainer;
+            rowContainer.children.push(colContainer.id);
+
+            if (!hasOverlap(lower, false)) {
+              lower.sort(sortByRowId).forEach((item) => {
+                const chartHolder = getChartHolder(item);
+                root[chartHolder.id] = chartHolder;
+                colContainer.children.push(chartHolder.id);
+              });
+            } else {
+              doConvert(lower, level + 2, colContainer, root);
+            }
+
+            // add col meta
+            colContainer.meta.width = getChildrenMax(colContainer.children, 'width', root);
+          }
+
+          currentItems = upper.slice();
+        }
+        currentCol++;
+      }
+    }
+
+    rowContainer.meta.width = getChildrenSum(rowContainer.children, 'width', root);
+  });
+}
+
+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) => {
+      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))));
+  let newSliceCounter = 0;
+  dashboard.slices.forEach((slice) => {
+    const sliceId = slice.slice_id;
+    let pos = posDict[sliceId];
+    if (!pos) {
+      // append new slices to dashboard bottom, 3 slices per row
+      pos = {
+        col: (newSliceCounter % 3) * 16 + 1,
+        row: lastRowId + Math.floor(newSliceCounter / 3) * 16,
+        size_x: 16,
+        size_y: 16,
+        slice_id: String(sliceId),
+      };
+      newSliceCounter++;
+    }
+
+    positions.push(pos);
+  });
+
+  const root = {
+    [DASHBOARD_ROOT_ID]: {
+      version: 'v2',
+      type: DASHBOARD_ROOT_TYPE,
+      id: DASHBOARD_ROOT_ID,
+      children: [DASHBOARD_GRID_ID],
+    },
+    [DASHBOARD_GRID_ID]: {
+      type: DASHBOARD_GRID_TYPE,
+      id: DASHBOARD_GRID_ID,
+      children: [],
+    },
+    [DASHBOARD_HEADER_ID]: {
+      type: DASHBOARD_HEADER_TYPE,
+      id: DASHBOARD_HEADER_ID,
+    },
+  };
+  doConvert(positions, 0, root[DASHBOARD_GRID_ID], root);
+
+  // remove row's width/height and col's height
+  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/actions/messageToasts.js b/superset/assets/src/dashboard/v2/actions/messageToasts.js
index af10ead..2ebc06c 100644
--- a/superset/assets/src/dashboard/v2/actions/messageToasts.js
+++ b/superset/assets/src/dashboard/v2/actions/messageToasts.js
@@ -6,7 +6,6 @@ function getToastUuid(type) {
 
 export const ADD_TOAST = 'ADD_TOAST';
 export function addToast({ toastType, text }) {
-  debugger;
   return {
     type: ADD_TOAST,
     payload: {
diff --git a/superset/assets/src/dashboard/v2/components/BuilderComponentPane.jsx b/superset/assets/src/dashboard/v2/components/BuilderComponentPane.jsx
index efef5a5..f9a37cc 100644
--- a/superset/assets/src/dashboard/v2/components/BuilderComponentPane.jsx
+++ b/superset/assets/src/dashboard/v2/components/BuilderComponentPane.jsx
@@ -1,37 +1,69 @@
 import React from 'react';
-import PropTypes from 'prop-types';
+import cx from 'classnames';
 
-import NewChart from './gridComponents/new/NewChart';
 import NewColumn from './gridComponents/new/NewColumn';
 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';
 
-const propTypes = {
-  editMode: PropTypes.bool,
-};
+import '../stylesheets/builder-sidepane.less';
 
 class BuilderComponentPane extends React.PureComponent {
+  constructor(props) {
+    super(props);
+    this.state = {
+      showSlices: false,
+    };
+
+    this.openSlicesPane = this.showSlices.bind(this, true);
+    this.closeSlicesPane = this.showSlices.bind(this, false);
+  }
+
+  showSlices(show) {
+    this.setState({
+      showSlices: show,
+    });
+  }
+
   render() {
     return (
       <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" />
+          }
         </div>
-        <NewChart />
-        <NewHeader />
 
-        <NewDivider />
+        <div className="component-layer">
+          <div
+            className="dragdroppable dragdroppable-row"
+            onClick={this.openSlicesPane}
+            role="none"
+          >
+            <div className="new-component static">
+              <div className="new-component-placeholder fa fa-area-chart" />
+              Chart
+              <i className="fa fa-arrow-right open trigger" />
+            </div>
+          </div>
 
-        <NewTabs />
-        <NewRow />
-        <NewColumn />
+          <NewHeader />
+          <NewDivider />
+
+          <NewTabs />
+          <NewRow />
+          <NewColumn />
+        </div>
+
+        <div className={cx('slices-layer', this.state.showSlices && 'show')}>
+          <SliceAdderContainer />
+        </div>
       </div>
     );
   }
 }
 
-BuilderComponentPane.propTypes = propTypes;
-
 export default BuilderComponentPane;
diff --git a/superset/assets/src/dashboard/v2/components/DashboardBuilder.jsx b/superset/assets/src/dashboard/v2/components/DashboardBuilder.jsx
index 8e2d985..f3f5867 100644
--- a/superset/assets/src/dashboard/v2/components/DashboardBuilder.jsx
+++ b/superset/assets/src/dashboard/v2/components/DashboardBuilder.jsx
@@ -20,15 +20,18 @@ import {
 } from '../util/constants';
 
 const propTypes = {
+  cells: PropTypes.object.isRequired,
+
   // redux
   dashboardLayout: PropTypes.object.isRequired,
   deleteTopLevelTabs: PropTypes.func.isRequired,
   editMode: PropTypes.bool.isRequired,
+  showBuilderPane: PropTypes.bool,
   handleComponentDrop: PropTypes.func.isRequired,
 };
 
 const defaultProps = {
-  editMode: true,
+  showBuilderPane: false,
 };
 
 class DashboardBuilder extends React.Component {
@@ -105,6 +108,7 @@ class DashboardBuilder extends React.Component {
               index={0}
               renderTabContent={false}
               onChangeTab={this.handleChangeTab}
+              cells={this.props.cells}
             />
           </WithPopoverMenu>}
 
@@ -112,8 +116,11 @@ class DashboardBuilder extends React.Component {
           <DashboardGrid
             gridComponent={gridComponent}
             depth={DASHBOARD_ROOT_DEPTH + 1}
+            cells={this.props.cells}
           />
-          {editMode && <BuilderComponentPane />}
+          {this.props.editMode && this.props.showBuilderPane &&
+            <BuilderComponentPane />
+          }
         </div>
         <ToastPresenter />
       </div>
diff --git a/superset/assets/src/dashboard/v2/components/DashboardGrid.jsx b/superset/assets/src/dashboard/v2/components/DashboardGrid.jsx
index 9f4cb93..2aa82af 100644
--- a/superset/assets/src/dashboard/v2/components/DashboardGrid.jsx
+++ b/superset/assets/src/dashboard/v2/components/DashboardGrid.jsx
@@ -71,7 +71,7 @@ class DashboardGrid extends React.PureComponent {
   }
 
   render() {
-    const { gridComponent, handleComponentDrop, depth, editMode } = this.props;
+    const { gridComponent, handleComponentDrop, depth, editMode, cells } = this.props;
     const { isResizing, rowGuideTop } = this.state;
 
     return (
@@ -93,6 +93,7 @@ class DashboardGrid extends React.PureComponent {
                     index={index}
                     availableColumnCount={GRID_COLUMN_COUNT}
                     columnWidth={columnWidth}
+                    cells={cells}
                     onResizeStart={this.handleResizeStart}
                     onResize={this.handleResize}
                     onResizeStop={this.handleResizeStop}
diff --git a/superset/assets/src/dashboard/v2/components/DashboardHeader.jsx b/superset/assets/src/dashboard/v2/components/DashboardHeader.jsx
index ca204e5..d3ec7ac 100644
--- a/superset/assets/src/dashboard/v2/components/DashboardHeader.jsx
+++ b/superset/assets/src/dashboard/v2/components/DashboardHeader.jsx
@@ -52,7 +52,7 @@ class DashboardHeader extends React.Component {
       <div className="dashboard-header">
         <div className="dashboard-component-header header-large">
           <EditableTitle
-            title={component.meta.text}
+            title={'Test title'}
             onSaveTitle={this.handleChangeText}
             showTooltip={false}
             canEdit={editMode}
diff --git a/superset/assets/src/dashboard/v2/components/dnd/dragDroppableConfig.js b/superset/assets/src/dashboard/v2/components/dnd/dragDroppableConfig.js
index 55d7e1d..54ce67e 100644
--- a/superset/assets/src/dashboard/v2/components/dnd/dragDroppableConfig.js
+++ b/superset/assets/src/dashboard/v2/components/dnd/dragDroppableConfig.js
@@ -17,6 +17,7 @@ export const dragConfig = [
       return {
         type: component.type,
         id: component.id,
+        meta: component.meta,
         index,
         parentId: parentComponent.id,
         parentType: parentComponent.type,
diff --git a/superset/assets/src/dashboard/v2/components/dnd/handleDrop.js b/superset/assets/src/dashboard/v2/components/dnd/handleDrop.js
index f27b604..7cb630d 100644
--- a/superset/assets/src/dashboard/v2/components/dnd/handleDrop.js
+++ b/superset/assets/src/dashboard/v2/components/dnd/handleDrop.js
@@ -35,6 +35,7 @@ export default function handleDrop(props, monitor, Component) {
     dragging: {
       id: draggingItem.id,
       type: draggingItem.type,
+      meta: draggingItem.meta,
     },
   };
 
diff --git a/superset/assets/src/dashboard/v2/components/gridComponents/Chart.jsx b/superset/assets/src/dashboard/v2/components/gridComponents/ChartHolder.jsx
similarity index 94%
rename from superset/assets/src/dashboard/v2/components/gridComponents/Chart.jsx
rename to superset/assets/src/dashboard/v2/components/gridComponents/ChartHolder.jsx
index 668d268..2aed4b2 100644
--- a/superset/assets/src/dashboard/v2/components/gridComponents/Chart.jsx
+++ b/superset/assets/src/dashboard/v2/components/gridComponents/ChartHolder.jsx
@@ -19,6 +19,7 @@ const propTypes = {
   index: PropTypes.number.isRequired,
   depth: PropTypes.number.isRequired,
   editMode: PropTypes.bool.isRequired,
+  chart: PropTypes.object.isRequired,
 
   // grid related
   availableColumnCount: PropTypes.number.isRequired,
@@ -35,7 +36,7 @@ const propTypes = {
 const defaultProps = {
 };
 
-class Chart extends React.Component {
+class ChartHolder extends React.Component {
   constructor(props) {
     super(props);
     this.state = {
@@ -112,7 +113,7 @@ class Chart extends React.Component {
               editMode={editMode}
             >
               <div className="dashboard-component dashboard-component-chart">
-                <div className="fa fa-area-chart" />
+                {this.props.chart}
               </div>
 
               {dropIndicatorProps && <div {...dropIndicatorProps} />}
@@ -124,7 +125,7 @@ class Chart extends React.Component {
   }
 }
 
-Chart.propTypes = propTypes;
-Chart.defaultProps = defaultProps;
+ChartHolder.propTypes = propTypes;
+ChartHolder.defaultProps = defaultProps;
 
-export default Chart;
+export default ChartHolder;
diff --git a/superset/assets/src/dashboard/v2/components/gridComponents/Column.jsx b/superset/assets/src/dashboard/v2/components/gridComponents/Column.jsx
index fe5a721..490d7bd 100644
--- a/superset/assets/src/dashboard/v2/components/gridComponents/Column.jsx
+++ b/superset/assets/src/dashboard/v2/components/gridComponents/Column.jsx
@@ -25,6 +25,7 @@ const propTypes = {
   index: PropTypes.number.isRequired,
   depth: PropTypes.number.isRequired,
   editMode: PropTypes.bool.isRequired,
+  cells: PropTypes.object.isRequired,
 
   // grid related
   availableColumnCount: PropTypes.number.isRequired,
@@ -92,6 +93,7 @@ class Column extends React.PureComponent {
       onResizeStop,
       handleComponentDrop,
       editMode,
+      cells,
     } = this.props;
 
     const columnItems = columnComponent.children || [];
@@ -154,19 +156,20 @@ class Column extends React.PureComponent {
                   </HoverMenu>}
 
                 {columnItems.map((componentId, itemIndex) => (
-                  <DashboardComponent
-                    key={componentId}
-                    id={componentId}
-                    parentId={columnComponent.id}
-                    depth={depth + 1}
-                    index={itemIndex}
-                    availableColumnCount={columnComponent.meta.width}
-                    columnWidth={columnWidth}
-                    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}
+                      cells={cells}
+                      onResizeStart={onResizeStart}
+                      onResize={onResize}
+                      onResizeStop={onResizeStop}
+                    />
+                  ))}
 
                 {dropIndicatorProps && <div {...dropIndicatorProps} />}
               </div>
diff --git a/superset/assets/src/dashboard/v2/components/gridComponents/Row.jsx b/superset/assets/src/dashboard/v2/components/gridComponents/Row.jsx
index 9866bc8..8faaee1 100644
--- a/superset/assets/src/dashboard/v2/components/gridComponents/Row.jsx
+++ b/superset/assets/src/dashboard/v2/components/gridComponents/Row.jsx
@@ -23,6 +23,7 @@ const propTypes = {
   index: PropTypes.number.isRequired,
   depth: PropTypes.number.isRequired,
   editMode: PropTypes.bool.isRequired,
+  cells: PropTypes.object.isRequired,
 
   // grid related
   availableColumnCount: PropTypes.number.isRequired,
@@ -92,6 +93,7 @@ class Row extends React.PureComponent {
       onResizeStop,
       handleComponentDrop,
       editMode,
+      cells,
     } = this.props;
 
     const rowItems = rowComponent.children || [];
@@ -142,19 +144,20 @@ class Row extends React.PureComponent {
                 </HoverMenu>}
 
               {rowItems.map((componentId, itemIndex) => (
-                <DashboardComponent
-                  key={componentId}
-                  id={componentId}
-                  parentId={rowComponent.id}
-                  depth={depth + 1}
-                  index={itemIndex}
-                  availableColumnCount={availableColumnCount - occupiedColumnCount}
-                  columnWidth={columnWidth}
-                  onResizeStart={onResizeStart}
-                  onResize={onResize}
-                  onResizeStop={onResizeStop}
-                />
-              ))}
+
+                  <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}
+                  />
+                ))}
 
               {dropIndicatorProps && <div {...dropIndicatorProps} />}
             </div>
diff --git a/superset/assets/src/dashboard/v2/components/gridComponents/index.js b/superset/assets/src/dashboard/v2/components/gridComponents/index.js
index 96c9a19..ef6d13f 100644
--- a/superset/assets/src/dashboard/v2/components/gridComponents/index.js
+++ b/superset/assets/src/dashboard/v2/components/gridComponents/index.js
@@ -9,7 +9,7 @@ import {
   TABS_TYPE,
 } from '../../util/componentTypes';
 
-import Chart from './Chart';
+import ChartHolder from './ChartHolder';
 import Column from './Column';
 import Divider from './Divider';
 import Header from './Header';
@@ -17,7 +17,7 @@ import Row from './Row';
 import Tab from './Tab';
 import Tabs from './Tabs';
 
-export { default as Chart } from './Chart';
+export { default as ChartHolder } from './ChartHolder';
 export { default as Column } from './Column';
 export { default as Divider } from './Divider';
 export { default as Header } from './Header';
@@ -26,7 +26,7 @@ export { default as Tab } from './Tab';
 export { default as Tabs } from './Tabs';
 
 export default {
-  [CHART_TYPE]: Chart,
+  [CHART_TYPE]: ChartHolder,
   [COLUMN_TYPE]: Column,
   [DIVIDER_TYPE]: Divider,
   [HEADER_TYPE]: Header,
diff --git a/superset/assets/src/dashboard/v2/containers/DashboardBuilder.jsx b/superset/assets/src/dashboard/v2/containers/DashboardBuilder.jsx
index b8d717e..62fc94a 100644
--- a/superset/assets/src/dashboard/v2/containers/DashboardBuilder.jsx
+++ b/superset/assets/src/dashboard/v2/containers/DashboardBuilder.jsx
@@ -7,10 +7,12 @@ import {
   handleComponentDrop,
 } from '../actions/dashboardLayout';
 
-function mapStateToProps({ dashboardLayout: undoableLayout, editMode }) {
+function mapStateToProps({ dashboardLayout: undoableLayout, dashboardState: dashboard }, ownProps) {
   return {
     dashboardLayout: undoableLayout.present,
-    editMode,
+    cells: ownProps.cells,
+    editMode: dashboard.editMode,
+    showBuilderPane: dashboard.showBuilderPane,
   };
 }
 
diff --git a/superset/assets/src/dashboard/v2/containers/DashboardComponent.jsx b/superset/assets/src/dashboard/v2/containers/DashboardComponent.jsx
index add5a6d..01f7805 100644
--- a/superset/assets/src/dashboard/v2/containers/DashboardComponent.jsx
+++ b/superset/assets/src/dashboard/v2/containers/DashboardComponent.jsx
@@ -6,7 +6,7 @@ import { connect } from 'react-redux';
 import ComponentLookup from '../components/gridComponents';
 import getTotalChildWidth from '../util/getChildWidth';
 import { componentShape } from '../util/propShapes';
-import { COLUMN_TYPE, ROW_TYPE } from '../util/componentTypes';
+import { CHART_TYPE, COLUMN_TYPE, ROW_TYPE } from '../util/componentTypes';
 import { GRID_MIN_COLUMN_COUNT } from '../util/constants';
 
 import {
@@ -25,14 +25,14 @@ const propTypes = {
   handleComponentDrop: PropTypes.func.isRequired,
 };
 
-function mapStateToProps({ dashboardLayout: undoableLayout, editMode }, ownProps) {
+function mapStateToProps({ dashboardLayout: undoableLayout, dashboardState: dashboard }, ownProps) {
   const dashboardLayout = undoableLayout.present;
-  const { id, parentId } = ownProps;
+  const { id, parentId, cells } = ownProps;
   const component = dashboardLayout[id];
   const props = {
     component,
     parentComponent: dashboardLayout[parentId],
-    editMode,
+    editMode: dashboard.editMode,
   };
 
   // rows and columns need more data about their child dimensions
@@ -51,6 +51,11 @@ function mapStateToProps({ dashboardLayout: undoableLayout, editMode }, ownProps
         );
       }
     });
+  } else if (props.component.type === CHART_TYPE) {
+    const chartId = props.component.meta && props.component.meta.chartId;
+    if (chartId) {
+      props.chart = cells[chartId];
+    }
   }
 
   return props;
diff --git a/superset/assets/src/dashboard/v2/containers/DashboardGrid.jsx b/superset/assets/src/dashboard/v2/containers/DashboardGrid.jsx
index 67b2396..2adc390 100644
--- a/superset/assets/src/dashboard/v2/containers/DashboardGrid.jsx
+++ b/superset/assets/src/dashboard/v2/containers/DashboardGrid.jsx
@@ -7,6 +7,13 @@ import {
   resizeComponent,
 } from '../actions/dashboardLayout';
 
+function mapStateToProps({ dashboardState: dashboard }, ownProps) {
+  return {
+    editMode: dashboard.editMode,
+    cells: ownProps.cells,
+  };
+}
+
 function mapDispatchToProps(dispatch) {
   return bindActionCreators({
     handleComponentDrop,
@@ -14,4 +21,4 @@ function mapDispatchToProps(dispatch) {
   }, dispatch);
 }
 
-export default connect(({ editMode }) => ({ editMode }), mapDispatchToProps)(DashboardGrid);
+export default connect(mapStateToProps, mapDispatchToProps)(DashboardGrid);
diff --git a/superset/assets/src/dashboard/v2/containers/DashboardHeader.jsx b/superset/assets/src/dashboard/v2/containers/DashboardHeader.jsx
index 8855d2c..cc8e944 100644
--- a/superset/assets/src/dashboard/v2/containers/DashboardHeader.jsx
+++ b/superset/assets/src/dashboard/v2/containers/DashboardHeader.jsx
@@ -2,32 +2,55 @@ import { ActionCreators as UndoActionCreators } from 'redux-undo';
 import { bindActionCreators } from 'redux';
 import { connect } from 'react-redux';
 
-import DashboardHeader from '../components/DashboardHeader';
-import { DASHBOARD_HEADER_ID } from '../util/constants';
-
+import DashboardHeader from '../../components/Header';
+import {
+  setEditMode,
+  toggleBuilderPane,
+  fetchFaveStar,
+  saveFaveStar,
+  fetchCharts,
+  startPeriodicRender,
+  updateDashboardTitle,
+  onChange,
+  onSave,
+} from '../../actions/dashboardState';
 import {
-  updateComponents,
   handleComponentDrop,
 } from '../actions/dashboardLayout';
 
-import { setEditMode } from '../actions/editMode';
-
-function mapStateToProps({ dashboardLayout: undoableLayout, editMode }) {
+function mapStateToProps({ dashboardLayout: undoableLayout, dashboardState: dashboard,
+                           dashboardInfo, charts }) {
   return {
-    component: undoableLayout.present[DASHBOARD_HEADER_ID],
+    dashboardInfo,
     canUndo: undoableLayout.past.length > 0,
     canRedo: undoableLayout.future.length > 0,
-    editMode,
+    layout: undoableLayout.present,
+    filters: dashboard.filters,
+    dashboardTitle: dashboard.title,
+    expandedSlices: dashboard.expandedSlices,
+    charts,
+    userId: dashboardInfo.userId,
+    isStarred: !!dashboard.isStarred,
+    hasUnsavedChanges: !!dashboard.hasUnsavedChanges,
+    editMode: !!dashboard.editMode,
+    showBuilderPane: !!dashboard.showBuilderPane,
   };
 }
 
 function mapDispatchToProps(dispatch) {
   return bindActionCreators({
-    updateComponents,
     handleComponentDrop,
     onUndo: UndoActionCreators.undo,
     onRedo: UndoActionCreators.redo,
     setEditMode,
+    toggleBuilderPane,
+    fetchFaveStar,
+    saveFaveStar,
+    fetchCharts,
+    startPeriodicRender,
+    updateDashboardTitle,
+    onChange,
+    onSave,
   }, dispatch);
 }
 
diff --git a/superset/assets/src/dashboard/v2/reducers/index.js b/superset/assets/src/dashboard/v2/reducers/index.js
index 731734d..061255d 100644
--- a/superset/assets/src/dashboard/v2/reducers/index.js
+++ b/superset/assets/src/dashboard/v2/reducers/index.js
@@ -1,17 +1,8 @@
-import { combineReducers } from 'redux';
 import undoable, { distinctState } from 'redux-undo';
 
 import dashboardLayout from './dashboardLayout';
-import editMode from './editMode';
-import messageToasts from './messageToasts';
 
-const undoableLayout = undoable(dashboardLayout, {
+export default undoable(dashboardLayout, {
   limit: 15,
   filter: distinctState(),
 });
-
-export default combineReducers({
-  dashboardLayout: undoableLayout,
-  editMode,
-  messageToasts,
-});
diff --git a/superset/assets/src/dashboard/v2/stylesheets/builder-sidepane.less b/superset/assets/src/dashboard/v2/stylesheets/builder-sidepane.less
new file mode 100644
index 0000000..d9f1069
--- /dev/null
+++ b/superset/assets/src/dashboard/v2/stylesheets/builder-sidepane.less
@@ -0,0 +1,103 @@
+.dashboard-builder-sidepane {
+  .trigger {
+    height: 25px;
+    width: 25px;
+    color: #879399;
+    position: relative;
+
+    &.close {
+      top: 3px;
+    }
+
+    &.open {
+      position: absolute;
+      right: 14px;
+    }
+  }
+
+  .component-layer {
+    .new-component.static {
+      cursor: pointer;
+    }
+  }
+
+  .slices-layer {
+    position: absolute;
+    width: 2px;
+    top: 51px;
+    right: 1px;
+    background: #fff;
+    transition-property: width;
+    transition-duration: 1s;
+    transition-timing-function: ease;
+    overflow: hidden;
+
+    &.show {
+      width: 374px;
+    }
+  }
+
+  .chart-card-container {
+    padding: 16px;
+    cursor: move;
+
+    .chart-card {
+      border: 1px solid #ccc;
+      height: 120px;
+      padding: 16px;
+      pointer-events: unset;
+    }
+
+    .chart-card.is-selected {
+      opacity: 0.45;
+      pointer-events: none;
+    }
+
+    .card-title {
+      margin-bottom: 8px;
+      font-weight: bold;
+    }
+
+    .card-body {
+      display: flex;
+      flex-direction: column;
+
+      .item {
+        height: 18px;
+      }
+
+      label {
+        margin-right: 5px;
+      }
+    }
+  }
+
+  .slice-adder-container {
+    .controls {
+      display: flex;
+      padding: 16px;
+
+      .dropdown.btn-group button,
+      input {
+        font-size: 14px;
+        line-height: 16px;
+        padding: 7px 12px;
+        height: 32px;
+      }
+
+      input {
+        margin-left: 16px;
+        width: 169px;
+        border: 1px solid #b3b3b3;
+
+        &:focus {
+          outline: none;
+        }
+      }
+    }
+
+    .ReactVirtualized__Grid.ReactVirtualized__List:focus {
+      outline: none;
+    }
+  }
+}
diff --git a/superset/assets/src/dashboard/v2/stylesheets/builder.less b/superset/assets/src/dashboard/v2/stylesheets/builder.less
index 3651c57..2ff99a4 100644
--- a/superset/assets/src/dashboard/v2/stylesheets/builder.less
+++ b/superset/assets/src/dashboard/v2/stylesheets/builder.less
@@ -1,5 +1,5 @@
 .dashboard-v2 {
-  margin-top: -20px;
+  //margin-top: -20px;
   position: relative;
   color: @almost-black;
 }
@@ -48,6 +48,7 @@
   flex: 0 0 376px;
   border: 1px solid @gray-light;
   z-index: 1;
+  position: relative;
 }
 
 .dashboard-builder-sidepane-header {
diff --git a/superset/assets/src/dashboard/v2/stylesheets/components/chart.less b/superset/assets/src/dashboard/v2/stylesheets/components/chart.less
index 141c3e9..ce03797 100644
--- a/superset/assets/src/dashboard/v2/stylesheets/components/chart.less
+++ b/superset/assets/src/dashboard/v2/stylesheets/components/chart.less
@@ -7,10 +7,11 @@
   display: flex;
   align-items: center;
   justify-content: center;
+  position: relative;
 }
 
 .dashboard-component-chart .fa {
-  font-size: 100px;
+  //font-size: 100px;
   opacity: 0.3;
 }
 
diff --git a/superset/assets/src/dashboard/v2/util/constants.js b/superset/assets/src/dashboard/v2/util/constants.js
index 36ef71b..f35614c 100644
--- a/superset/assets/src/dashboard/v2/util/constants.js
+++ b/superset/assets/src/dashboard/v2/util/constants.js
@@ -18,7 +18,7 @@ export const DASHBOARD_ROOT_DEPTH = 0;
 export const GRID_BASE_UNIT = 8;
 export const GRID_GUTTER_SIZE = 2 * GRID_BASE_UNIT;
 export const GRID_COLUMN_COUNT = 12;
-export const GRID_MIN_COLUMN_COUNT = 3;
+export const GRID_MIN_COLUMN_COUNT = 2;
 export const GRID_MIN_ROW_UNITS = 5;
 export const GRID_MAX_ROW_UNITS = 100;
 export const GRID_MIN_ROW_HEIGHT = GRID_GUTTER_SIZE;
diff --git a/superset/assets/src/dashboard/v2/util/newComponentFactory.js b/superset/assets/src/dashboard/v2/util/newComponentFactory.js
index af69eb8..b428ddd 100644
--- a/superset/assets/src/dashboard/v2/util/newComponentFactory.js
+++ b/superset/assets/src/dashboard/v2/util/newComponentFactory.js
@@ -34,7 +34,7 @@ function uuid(type) {
   return `${type}-${Math.random().toString(16)}`;
 }
 
-export default function entityFactory(type) {
+export default function entityFactory(type, meta) {
   return {
     version: 'v0',
     type,
@@ -42,6 +42,7 @@ export default function entityFactory(type) {
     children: [],
     meta: {
       ...typeToDefaultMetaData[type],
+      ...meta,
     },
   };
 }
diff --git a/superset/assets/src/dashboard/v2/util/newEntitiesFromDrop.js b/superset/assets/src/dashboard/v2/util/newEntitiesFromDrop.js
index 9e49643..7cccc5f 100644
--- a/superset/assets/src/dashboard/v2/util/newEntitiesFromDrop.js
+++ b/superset/assets/src/dashboard/v2/util/newEntitiesFromDrop.js
@@ -11,9 +11,10 @@ export default function newEntitiesFromDrop({ dropResult, components }) {
   const { dragging, destination } = dropResult;
 
   const dragType = dragging.type;
+  const dragMeta = dragging.meta;
   const dropEntity = components[destination.id];
   const dropType = dropEntity.type;
-  let newDropChild = newComponentFactory(dragType);
+  let newDropChild = newComponentFactory(dragType, dragMeta);
   const wrapChildInRow = shouldWrapChildInRow({ parentType: dropType, childType: dragType });
 
   const newEntities = {
diff --git a/superset/assets/src/dashboard/v2/util/propShapes.jsx b/superset/assets/src/dashboard/v2/util/propShapes.jsx
index 8acc192..388c726 100644
--- a/superset/assets/src/dashboard/v2/util/propShapes.jsx
+++ b/superset/assets/src/dashboard/v2/util/propShapes.jsx
@@ -16,7 +16,6 @@ export const componentShape = PropTypes.shape({ // eslint-disable-line
     height: PropTypes.number,
 
     // Header
-    text: PropTypes.string,
     headerSize: PropTypes.oneOf(headerStyleOptions.map(opt => opt.value)),
 
     // Row
@@ -29,3 +28,52 @@ export const toastShape = PropTypes.shape({
   toastType: PropTypes.oneOf([INFO_TOAST, SUCCESS_TOAST, WARNING_TOAST, DANGER_TOAST]).isRequired,
   text: PropTypes.string.isRequired,
 });
+
+export const chartPropShape = PropTypes.shape({
+  id: PropTypes.number.isRequired,
+  chartAlert: PropTypes.string,
+  chartStatus: PropTypes.string,
+  chartUpdateEndTime: PropTypes.number,
+  chartUpdateStartTime: PropTypes.number,
+  latestQueryFormData: PropTypes.object,
+  queryRequest: PropTypes.object,
+  queryResponse: PropTypes.object,
+  triggerQuery: PropTypes.bool,
+  lastRendered: PropTypes.number,
+});
+
+export const slicePropShape = PropTypes.shape({
+  slice_id: PropTypes.number.isRequired,
+  slice_url: PropTypes.string.isRequired,
+  slice_name: PropTypes.string.isRequired,
+  edit_url: PropTypes.string.isRequired,
+  datasource: PropTypes.string,
+  datasource_name: PropTypes.string,
+  datasource_link: PropTypes.string,
+  changedOn: PropTypes.number,
+  modified: PropTypes.string,
+  viz_type: PropTypes.string.isRequired,
+  description: PropTypes.string,
+  description_markeddown: PropTypes.string,
+});
+
+export const dashboardStatePropShape = PropTypes.shape({
+  title: PropTypes.string.isRequired,
+  sliceIds: PropTypes.object.isRequired,
+  refresh: PropTypes.bool.isRequired,
+  filters: PropTypes.object,
+  expandedSlices: PropTypes.object,
+  editMode: PropTypes.bool,
+  showBuilderPane: PropTypes.bool,
+  hasUnsavedChanges: PropTypes.bool,
+});
+
+export const dashboardInfoPropShape = PropTypes.shape({
+  id: PropTypes.number.isRequired,
+  metadata: PropTypes.object,
+  slug: PropTypes.string,
+  dash_edit_perm: PropTypes.bool.isRequired,
+  dash_save_perm: PropTypes.bool.isRequired,
+  common: PropTypes.object,
+  userId: PropTypes.string.isRequired,
+});
diff --git a/superset/assets/src/explore/components/ExploreChartHeader.jsx b/superset/assets/src/explore/components/ExploreChartHeader.jsx
index 69871dc..19416b0 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 { chartPropType } from '../../chart/chartReducer';
+import { chartPropShape } from '../../dashboard/v2/util/propShapes';
 import ExploreActionButtons from './ExploreActionButtons';
 import RowCountLabel from './RowCountLabel';
 import EditableTitle from '../../components/EditableTitle';
@@ -28,13 +28,13 @@ const propTypes = {
   table_name: PropTypes.string,
   form_data: PropTypes.object,
   timeout: PropTypes.number,
-  chart: PropTypes.shape(chartPropType),
+  chart: chartPropShape,
 };
 
 class ExploreChartHeader extends React.PureComponent {
   runQuery() {
     this.props.actions.runQuery(this.props.form_data, true,
-      this.props.timeout, this.props.chart.chartKey);
+      this.props.timeout, this.props.chart.id);
   }
 
   updateChartTitleOrSaveSlice(newTitle) {
diff --git a/superset/assets/src/explore/components/ExploreChartPanel.jsx b/superset/assets/src/explore/components/ExploreChartPanel.jsx
index bfb24ff..21c6a64 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 { chartPropType } from '../../chart/chartReducer';
+import { chartPropShape } from '../../dashboard/v2/util/propShapes';
 import ChartContainer from '../../chart/ChartContainer';
 import ExploreChartHeader from './ExploreChartHeader';
 
@@ -27,7 +27,7 @@ const propTypes = {
   standalone: PropTypes.bool,
   timeout: PropTypes.number,
   refreshOverlayVisible: PropTypes.bool,
-  chart: PropTypes.shape(chartPropType),
+  chart: chartPropShape,
   errorMessage: PropTypes.node,
 };
 
@@ -45,7 +45,7 @@ class ExploreChartPanel extends React.PureComponent {
         formData={this.props.form_data}
         height={this.getHeight()}
         slice={this.props.slice}
-        chartKey={this.props.chart.chartKey}
+        chartId={this.props.chart.id}
         setControlValue={this.props.actions.setControlValue}
         timeout={this.props.timeout}
         vizType={this.props.vizType}
diff --git a/superset/assets/src/explore/components/ExploreViewContainer.jsx b/superset/assets/src/explore/components/ExploreViewContainer.jsx
index bc875d4..3e761eb 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 '../store';
-import { chartPropType } from '../../chart/chartReducer';
+import { chartPropShape } from '../../dashboard/v2/util/propShapes';
 import * as exploreActions from '../actions/exploreActions';
 import * as saveModalActions from '../actions/saveModalActions';
 import * as chartActions from '../../chart/chartAction';
@@ -22,7 +22,7 @@ const propTypes = {
   actions: PropTypes.object.isRequired,
   datasource_type: PropTypes.string.isRequired,
   isDatasourceMetaLoading: PropTypes.bool.isRequired,
-  chart: PropTypes.shape(chartPropType).isRequired,
+  chart: chartPropShape.isRequired,
   slice: PropTypes.object,
   controls: PropTypes.object.isRequired,
   forcedHeight: PropTypes.string,
@@ -72,7 +72,7 @@ class ExploreViewContainer extends React.Component {
     }
     if (np.controls.viz_type.value !== this.props.controls.viz_type.value) {
       this.props.actions.resetControls();
-      this.props.actions.triggerQuery(true, this.props.chart.chartKey);
+      this.props.actions.triggerQuery(true, this.props.chart.id);
     }
     if (
       np.controls.datasource && (
@@ -86,8 +86,8 @@ class ExploreViewContainer extends React.Component {
     const changedControlKeys = this.findChangedControlKeys(this.props.controls, np.controls);
     if (this.hasDisplayControlChanged(changedControlKeys, np.controls)) {
       this.props.actions.updateQueryFormData(
-        getFormDataFromControls(np.controls), this.props.chart.chartKey);
-      this.props.actions.renderTriggered(new Date().getTime(), this.props.chart.chartKey);
+        getFormDataFromControls(np.controls), this.props.chart.id);
+      this.props.actions.renderTriggered(new Date().getTime(), this.props.chart.id);
     }
     if (this.hasQueryControlChanged(changedControlKeys, np.controls)) {
       this.setState({ chartIsStale: true, refreshOverlayVisible: true });
@@ -112,7 +112,7 @@ class ExploreViewContainer extends React.Component {
   onQuery() {
     // remove alerts when query
     this.props.actions.removeControlPanelAlert();
-    this.props.actions.triggerQuery(true, this.props.chart.chartKey);
+    this.props.actions.triggerQuery(true, this.props.chart.id);
 
     this.setState({ chartIsStale: false, refreshOverlayVisible: false });
     this.addHistory({});
@@ -158,7 +158,7 @@ class ExploreViewContainer extends React.Component {
   triggerQueryIfNeeded() {
     if (this.props.chart.triggerQuery && !this.hasErrors()) {
       this.props.actions.runQuery(this.props.form_data, false,
-        this.props.timeout, this.props.chart.chartKey);
+        this.props.timeout, this.props.chart.id);
     }
   }
 
@@ -198,7 +198,7 @@ class ExploreViewContainer extends React.Component {
         formData,
         false,
         this.props.timeout,
-        this.props.chart.chartKey,
+        this.props.chart.id,
       );
     }
   }
diff --git a/superset/assets/src/explore/exploreUtils.js b/superset/assets/src/explore/exploreUtils.js
index 1c1271b..fcab33f 100644
--- a/superset/assets/src/explore/exploreUtils.js
+++ b/superset/assets/src/explore/exploreUtils.js
@@ -3,7 +3,7 @@ import URI from 'urijs';
 
 export function getChartKey(explore) {
   const slice = explore.slice;
-  return slice ? ('slice_' + slice.slice_id) : 'slice';
+  return slice ? (slice.slice_id) : 0;
 }
 
 export function getAnnotationJsonUrl(slice_id, form_data, isNative) {
diff --git a/superset/assets/src/explore/index.jsx b/superset/assets/src/explore/index.jsx
index 5989b5f..07870e4 100644
--- a/superset/assets/src/explore/index.jsx
+++ b/superset/assets/src/explore/index.jsx
@@ -49,7 +49,7 @@ const chartKey = getChartKey(bootstrappedState);
 const initState = {
   charts: {
     [chartKey]: {
-      chartKey,
+      id: chartKey,
       chartAlert: null,
       chartStatus: 'loading',
       chartUpdateEndTime: null,
diff --git a/superset/assets/src/explore/reducers/index.js b/superset/assets/src/explore/reducers/index.js
index 13d0ed1..953b0b5 100644
--- a/superset/assets/src/explore/reducers/index.js
+++ b/superset/assets/src/explore/reducers/index.js
@@ -1,13 +1,14 @@
 import { combineReducers } from 'redux';
-import shortid from 'shortid';
 
 import charts from '../../chart/chartReducer';
 import saveModal from './saveModalReducer';
 import explore from './exploreReducer';
 
+const impressionId = (state = '') => (state);
+
 export default combineReducers({
   charts,
   saveModal,
   explore,
-  impressionId: () => (shortid.generate()),
+  impressionId,
 });
diff --git a/superset/assets/src/modules/utils.js b/superset/assets/src/modules/utils.js
index 016444a..c4ea9ce 100644
--- a/superset/assets/src/modules/utils.js
+++ b/superset/assets/src/modules/utils.js
@@ -165,7 +165,6 @@ export const controllerInterface = {
   addFiler: () => {},
   setFilter: () => {},
   getFilters: () => false,
-  clearFilter: () => {},
   removeFilter: () => {},
   filters: {},
 };
diff --git a/superset/assets/src/visualizations/table.css b/superset/assets/src/visualizations/table.css
index a5b8462..9af0c0e 100644
--- a/superset/assets/src/visualizations/table.css
+++ b/superset/assets/src/visualizations/table.css
@@ -30,11 +30,10 @@ table.table thead th.sorting:after, table.table thead th.sorting_asc:after, tabl
   white-space: pre-wrap;
 }
 
+.widget.table {
+  width: auto;
+  max-width: unset;
+}
 .widget.table thead tr {
   height: 25px;
 }
-
-.dashboard .slice_container.table {
-  padding-left: 10px;
-  padding-right: 10px;
-}
diff --git a/superset/assets/stylesheets/dashboard.css b/superset/assets/stylesheets/dashboard.less
similarity index 58%
rename from superset/assets/stylesheets/dashboard.css
rename to superset/assets/stylesheets/dashboard.less
index c1f08a7..b812a42 100644
--- a/superset/assets/stylesheets/dashboard.css
+++ b/superset/assets/stylesheets/dashboard.less
@@ -1,3 +1,5 @@
+@import "./less/cosmo/variables.less";
+
 .dashboard a i {
   cursor: pointer;
 }
@@ -11,28 +13,58 @@
   border-color: #AAA;
   opacity: 0.3;
 }
-div.widget .chart-controls {
-  background-clip: content-box;
+.dashboard .widget {
   position: absolute;
-  z-index: 100;
-  right: 0;
-  top: 5px;
-  padding: 5px 5px;
-  opacity: 0;
-  transition: opacity 0.5s ease-in-out;
-}
-div.widget:hover .chart-controls {
-  opacity: 0.75;
-  transition: opacity 0.5s ease-in-out;
-}
-.slice-grid div.widget {
-  border-radius: 0;
-  border: 0;
+  top: 16px;
+  left: 16px;
   box-shadow: none;
-  background-color: #fff;
+  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;
 }
@@ -73,26 +105,16 @@ div.widget:hover .chart-controls {
   display: none;
 }
 
-.slice-grid div.separator.widget {
- border: 1px solid transparent;
-  box-shadow: none;
-  z-index: 1;
-}
-.slice-grid div.separator.widget:hover {
-  border: 1px solid #EEE;
-}
-.slice-grid div.separator.widget .chart-header {
-  background-color: transparent;
-  color: transparent;
-}
-.slice-grid div.separator.widget h1,h2,h3,h4 {
-  margin-top: 0px;
-}
-
 .slice-cell {
   box-shadow: 0px 0px 20px 5px rgba(0,0,0,0);
   transition: box-shadow 1s ease-in;
-  height: 100%;
+
+  .dropdown,
+  .dropdown-menu {
+    .fa {
+      font-size: 14px;
+    }
+  }
 }
 
 .slice-cell-highlight {
@@ -104,30 +126,8 @@ div.widget:hover .chart-controls {
   font-weight: bold;
 }
 
-.dashboard .separator.widget .slice_container {
-  padding: 0;
-  overflow: visible;
-}
-.dashboard .separator.widget .slice_container hr {
-  margin-top: 5px;
-  margin-bottom: 5px;
-}
-.separator .chart-container {
-  position: absolute;
-  left: 0;
-  right: 0;
-  top: 0;
-  bottom: 0;
-}
-
-.dashboard .title {
-  margin: 0 20px;
-}
-
-.dashboard .title .favstar {
-  font-size: 20px;
-  position: relative;
-  top: -5px;
+.chart-container {
+  box-sizing: border-box;
 }
 
 .chart-header .header {
diff --git a/superset/assets/stylesheets/superset.less b/superset/assets/stylesheets/superset.less
index 743daa8..6987544 100644
--- a/superset/assets/stylesheets/superset.less
+++ b/superset/assets/stylesheets/superset.less
@@ -162,7 +162,6 @@ li.widget:hover {
 div.widget .chart-header {
   padding-top: 8px;
   color: #333;
-  border-bottom: 1px solid #aaa;
   margin: 0 10px;
 }
 
@@ -177,10 +176,6 @@ div.widget .chart-header {
 }
 
 
-div.widget .chart-header a {
-  margin-left: 5px;
-}
-
 #is_cached {
   display: none;
 }
@@ -458,6 +453,17 @@ g.annotation-container {
   border-color: @brand-primary;
 }
 
+.fave-unfave-icon {
+  .fa-star-o,
+  .fa-star {
+    &,
+    &:hover,
+    &:active {
+      color: #263238;
+    }
+  }
+}
+
 .metric-edit-popover-label-input {
   border-radius: 4px;
   height: 30px;
diff --git a/superset/models/core.py b/superset/models/core.py
index 4674520..b450be0 100644
--- a/superset/models/core.py
+++ b/superset/models/core.py
@@ -146,6 +146,11 @@ class Slice(Model, AuditMixinNullable, ImportMixin):
         datasource = self.datasource
         return datasource.link if datasource else None
 
+    def datasource_name_text(self):
+        # pylint: disable=no-member
+        datasource = self.datasource
+        return datasource.name if datasource else None
+
     @property
     def datasource_edit_url(self):
         # pylint: disable=no-member
@@ -338,14 +343,6 @@ class Dashboard(Model, AuditMixinNullable, ImportMixin):
 
     @property
     def url(self):
-        if self.json_metadata:
-            # add default_filters to the preselect_filters of dashboard
-            json_metadata = json.loads(self.json_metadata)
-            default_filters = json_metadata.get('default_filters')
-            if default_filters:
-                filters = parse.quote(default_filters.encode('utf8'))
-                return '/superset/dashboard/{}/?preselect_filters={}'.format(
-                    self.slug or self.id, filters)
         return '/superset/dashboard/{}/'.format(self.slug or self.id)
 
     @property
diff --git a/superset/templates/superset/dashboard.html b/superset/templates/superset/dashboard.html
index 25633da..1a158d9 100644
--- a/superset/templates/superset/dashboard.html
+++ b/superset/templates/superset/dashboard.html
@@ -3,6 +3,7 @@
 {% block body %}
 <div
   id="app"
+  class="dashboard container-fluid"
   data-bootstrap="{{ bootstrap_data }}"
 >
 </div>
diff --git a/superset/views/core.py b/superset/views/core.py
index 3a0f28f..bd7715d 100755
--- a/superset/views/core.py
+++ b/superset/views/core.py
@@ -518,9 +518,10 @@ appbuilder.add_view_no_menu(SliceAsync)
 
 class SliceAddView(SliceModelView):  # noqa
     list_columns = [
-        'id', 'slice_name', 'slice_link', 'viz_type',
-        'datasource_link', 'owners', 'modified', 'changed_on']
-    show_columns = list(set(SliceModelView.edit_columns + list_columns))
+        'id', 'slice_name', 'slice_url', 'edit_url', 'viz_type', 'params',
+        'description', 'description_markeddown',
+        'datasource_name_text', 'datasource_link',
+        'owners', 'modified', 'changed_on']
 
 
 appbuilder.add_view_no_menu(SliceAddView)
@@ -1593,9 +1594,17 @@ class Superset(BaseSupersetView):
     @staticmethod
     def _set_dash_metadata(dashboard, data):
         positions = data['positions']
-        slice_ids = [int(d['slice_id']) for d in positions]
-        dashboard.slices = [o for o in dashboard.slices if o.id in slice_ids]
-        positions = sorted(data['positions'], key=lambda x: int(x['slice_id']))
+        # find slices in the position data
+        slice_ids = []
+        for value in positions.values():
+            if value.get('meta') and value.get('meta').get('chartId'):
+                slice_ids.append(int(value.get('meta').get('chartId')))
+        session = db.session()
+        Slice = models.Slice  # noqa
+        current_slices = session.query(Slice).filter(
+            Slice.id.in_(slice_ids)).all()
+
+        dashboard.slices = current_slices
         dashboard.position_json = json.dumps(positions, indent=4, sort_keys=True)
         md = dashboard.params_dict
         dashboard.css = data['css']
@@ -1608,7 +1617,11 @@ class Superset(BaseSupersetView):
         if 'filter_immune_slice_fields' not in md:
             md['filter_immune_slice_fields'] = {}
         md['expanded_slices'] = data['expanded_slices']
-        md['default_filters'] = data.get('default_filters', '')
+        default_filters_data = json.loads(data.get('default_filters', ''))
+        for key in default_filters_data.keys():
+            if int(key) not in slice_ids:
+                del default_filters_data[key]
+        md['default_filters'] = json.dumps(default_filters_data)
         dashboard.json_metadata = json.dumps(md, indent=4)
 
     @api