You are viewing a plain text version of this content. The canonical link for it is here.
Posted to dev@superset.apache.org by GitBox <gi...@apache.org> on 2018/04/19 22:27:25 UTC

[GitHub] williaster closed pull request #4849: Dashboard builder rebased + linted

williaster closed pull request #4849: Dashboard builder rebased + linted
URL: https://github.com/apache/incubator-superset/pull/4849
 
 
   

This is a PR merged from a forked repository.
As GitHub hides the original diff on merge, it is displayed below for
the sake of provenance:

As this is a foreign pull request (from a fork), the diff is supplied
below (as it won't show otherwise due to GitHub magic):

diff --git a/superset/assets/package.json b/superset/assets/package.json
index a5a2f5394d..b45846666b 100644
--- a/superset/assets/package.json
+++ b/superset/assets/package.json
@@ -89,19 +89,18 @@
     "react-addons-css-transition-group": "^15.6.0",
     "react-addons-shallow-compare": "^15.4.2",
     "react-alert": "^2.3.0",
-    "react-bootstrap": "^0.32.0",
+    "react-bootstrap": "^0.31.5",
     "react-bootstrap-slider": "2.0.1",
-    "react-bootstrap-table": "^4.0.2",
     "react-color": "^2.13.8",
     "react-datetime": "2.9.0",
     "react-dnd": "^2.5.4",
     "react-dnd-html5-backend": "^2.5.4",
     "react-dom": "^15.6.2",
     "react-gravatar": "^2.6.1",
-    "react-grid-layout": "0.16.5",
     "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.6.7",
diff --git a/superset/assets/spec/javascripts/chart/Chart_spec.jsx b/superset/assets/spec/javascripts/chart/Chart_spec.jsx
index b766d9f8f4..29a2941870 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 c6e94d87d9..f4def13307 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 371b02c02c..1565ccda48 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 6421fec83c..580a574deb 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 b69ee3ae3b..e2088357be 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 b731412fc5..e3cb1f97e5 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 a2f01165d6..9d519262b9 100644
--- a/superset/assets/src/chart/chartAction.js
+++ b/superset/assets/src/chart/chartAction.js
@@ -112,6 +112,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) => {
@@ -134,7 +139,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,
@@ -185,3 +190,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 f68a5b80ee..d57959a1c0 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 c7f1a6aa26..0000000000
--- 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 0000000000..226272976b
--- /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 0000000000..a00bb17f3e
--- /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 0000000000..3a1b1dc02c
--- /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 0000000000..aaae4dffd3
--- /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 00cb6d56dc..8755e8f74d 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,21 +99,21 @@ class Controls extends React.PureComponent {
               <MenuItemContent
                 text={t('Set autorefresh')}
                 tooltip={t('Set the auto-refresh interval for this session')}
-                faIcon="clock-o"
               />
             }
           />
           <SaveModal
-            dashboard={dashboard}
+            dashboardId={this.props.dashboardInfo.id}
+            dashboardTitle={dashboardTitle}
+            layout={layout}
             filters={filters}
-            serialize={serialize}
+            expandedSlices={expandedSlices}
             onSave={onSave}
             css={this.state.css}
             triggerNode={
               <MenuItemContent
                 text={saveText}
                 tooltip={t('Save the dashboard')}
-                faIcon="save"
               />
             }
           />
@@ -161,8 +121,7 @@ class Controls extends React.PureComponent {
             <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 &&
@@ -170,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 Slices')}
-                  tooltip={t('Add some slices 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 2a6a227997..939476c62a 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,30 +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);
-    const extraFilters = this.effectiveExtraFilters(slice.slice_id);
-    formDataExtra.extra_filters = formDataExtra.filters.concat(extraFilters);
+  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() {
@@ -160,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)) {
@@ -196,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 d42946153f..31fe0350ad 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 91fe83943b..327327277c 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 ef0ec24796..fd561e29cd 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 52d3024ff9..11c8991ce2 100644
--- a/superset/assets/src/dashboard/components/Header.jsx
+++ b/superset/assets/src/dashboard/components/Header.jsx
@@ -1,46 +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 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 (
@@ -53,60 +72,90 @@ 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' }}
+        bsSize="small"
         onClick={this.toggleEditMode}
       >
         {btnText}
       </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}
+      <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 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>
+          </span>
+          {this.renderUnsaved()}
         </div>
-        <div className="pull-right" style={{ marginTop: '35px' }}>
-          {this.renderEditButton()}
+        <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 4cba010d95..2737a42c17 100644
--- a/superset/assets/src/dashboard/components/RefreshIntervalModal.jsx
+++ b/superset/assets/src/dashboard/components/RefreshIntervalModal.jsx
@@ -43,8 +43,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 da465a0057..6a69361fae 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 d5be8caff6..6477fc441a 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 slices 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 slices 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 Slices')}
-          </button>
+          }
         </div>
       </div>
     );
-
-    return (
-      <ModalTrigger
-        triggerNode={this.props.triggerNode}
-        tooltip={t('Add a new slice to the dashboard')}
-        beforeOpen={this.onEnterModal.bind(this)}
-        isMenuItem
-        modalBody={modalContent}
-        bsSize="large"
-        setModalAsTriggerChildren
-        modalTitle={t('Add Slices 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 0000000000..b4f10d9557
--- /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 8abcc86d61..264542a108 100644
--- a/superset/assets/src/dashboard/components/SliceHeader.jsx
+++ b/superset/assets/src/dashboard/components/SliceHeader.jsx
@@ -1,17 +1,16 @@
 import React from 'react';
 import PropTypes from 'prop-types';
-import moment from 'moment';
 
 import { t } from '../../locales';
 import EditableTitle from '../../components/EditableTitle';
 import TooltipWrapper from '../../components/TooltipWrapper';
+import SliceHeaderControls from './SliceHeaderControls';
 
 const propTypes = {
   slice: PropTypes.object.isRequired,
   isExpanded: PropTypes.bool,
   isCached: PropTypes.bool,
   cachedDttm: PropTypes.string,
-  removeSlice: PropTypes.func,
   updateSliceName: PropTypes.func,
   toggleExpandSlice: PropTypes.func,
   forceRefresh: PropTypes.func,
@@ -37,11 +36,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) {
@@ -50,17 +44,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.');
 
@@ -92,79 +81,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>
-              }
-              <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>
-              <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>
diff --git a/superset/assets/src/dashboard/components/SliceHeaderControls.jsx b/superset/assets/src/dashboard/components/SliceHeaderControls.jsx
new file mode 100644
index 0000000000..e98f69efa7
--- /dev/null
+++ b/superset/assets/src/dashboard/components/SliceHeaderControls.jsx
@@ -0,0 +1,103 @@
+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');
+
+    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 1aadc58126..9c00f9e33e 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 074c44524a..0000000000
--- 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/stores/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 0000000000..84ee58e559
--- /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 0000000000..4df75071b8
--- /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 0000000000..11292100ae
--- /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 0000000000..a2397e09be
--- /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 0000000000..61a58f6628
--- /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 0000000000..c9a6021525
--- /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 0000000000..854ca65e05
--- /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 af10eadf04..2ebc06c7e6 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 efef5a59a1..f9a37ccb27 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 8e2d985861..f3f58673da 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 9f4cb9317f..2aa82af2d7 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 ca204e5b71..d3ec7ac26a 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 55d7e1d3d8..54ce67e1a0 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 f27b604f54..7cb630d016 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 668d26888a..2aed4b2aad 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 fe5a721e6f..490d7bdd37 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 9866bc8d9b..8faaee110b 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 96c9a19ad0..ef6d13f935 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 b8d717e617..62fc94a09e 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 add5a6df3f..01f78052f4 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 67b2396e20..2adc390a5e 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 8855d2cd97..cc8e944a42 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 731734d23f..061255db0a 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 0000000000..d9f106975c
--- /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 3651c57368..2ff99a4da6 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 141c3e9f31..ce0379731b 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 36ef71b70d..f35614c269 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 af69eb8421..b428ddd35f 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 9e49643650..7cccc5f1dc 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 8acc1923d2..388c7266b8 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 69871dc58f..19416b0762 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 bfb24fff7f..21c6a644fb 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 1fec139c54..d4e718b348 100644
--- a/superset/assets/src/explore/components/ExploreViewContainer.jsx
+++ b/superset/assets/src/explore/components/ExploreViewContainer.jsx
@@ -11,7 +11,7 @@ import QueryAndSaveBtns from './QueryAndSaveBtns';
 import { getExploreUrlAndPayload, getExploreLongUrl } from '../exploreUtils';
 import { areObjectsEqual } from '../../reduxUtils';
 import { getFormDataFromControls } from '../stores/store';
-import { 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.value !== this.props.controls.datasource.value) {
       this.props.actions.fetchDatasourceMetadata(np.form_data.datasource, true);
@@ -81,8 +81,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 });
@@ -107,7 +107,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({});
@@ -153,7 +153,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);
     }
   }
 
@@ -193,7 +193,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 1c1271b045..fcab33f121 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 74f5a0bb68..575c9ab30d 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 13d0ed1b0b..953b0b5fca 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 016444a9f7..c4ea9cefc5 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 a5b8462c53..9af0c0e5f5 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 c1f08a7e38..b812a424cd 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 743daa8110..69875445e9 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 d82632873e..d1c94372e1 100644
--- a/superset/models/core.py
+++ b/superset/models/core.py
@@ -146,6 +146,11 @@ def datasource_link(self):
         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 @@ def table_names(self):
 
     @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 25633dad33..1a158d92a7 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 f66b70ef55..fc6012ba8d 100755
--- a/superset/views/core.py
+++ b/superset/views/core.py
@@ -517,9 +517,10 @@ class SliceAsync(SliceModelView):  # noqa
 
 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)
@@ -1588,9 +1589,17 @@ def save_dash(self, dashboard_id):
     @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']
@@ -1603,7 +1612,11 @@ def _set_dash_metadata(dashboard, data):
         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


 

----------------------------------------------------------------
This is an automated message from the Apache Git Service.
To respond to the message, please log on GitHub and use the
URL above to go to the specific comment.
 
For queries about this service, please contact Infrastructure at:
users@infra.apache.org


With regards,
Apache Git Services