superset-commits mailing list archives

Site index · List index
Message view « Date » · « Thread »
Top « Date » · « Thread »
From ccwilli...@apache.org
Subject [incubator-superset] 17/26: [dashboard v2] add v1 switch (#5126)
Date Fri, 22 Jun 2018 00:54:32 GMT
This is an automated email from the ASF dual-hosted git repository.

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

commit 0e0c76881dcc7ce6cadd23dfabaef01970e32379
Author: Chris Williams <williaster@users.noreply.github.com>
AuthorDate: Wed Jun 6 14:10:37 2018 -0700

    [dashboard v2] add v1 switch (#5126)
    
    * [dashboard] copy all dashboard v1 into working v1 switch
    
    * [dashboard] add functional v1 <> v2 switch with messaging
    
    * [dashboard] add v2 logging to v1 dashboard, add read-v2-changes link, add client logging to track v1 <> v2 switches
    
    * [dashboard] Remove default values for feedback url + v2 auto convert date
    
    * [dashboard v2] fix misc UI/UX issues
    
    * [dashboard v2] fix Markdown persistance issues and css, fix copy dash title, don't enforce shallow hovering with drop indicator
    
    * [dashboard v2] improve non-shallow drop target UX, fix Markdown drop indicator, clarify slice adder filter/sort
    
    * [dashboard v2] delete empty rows on drag or delete events that leave them without children, add test
    
    * [dashboard v2] improve v1<>v2 switch modals, add convert to v2 badge in v1, fix unsaved changes issue in preview mode, don't auto convert column child widths for now
    
    * [dashboard v2][dnd] add drop position cache to fix non-shallow drops
    
    * [dashboard] fix test script with glob instead of recurse, fix tests, add temp fix for tab nesting, ignore v1 lint errors
    
    * [dashboard] v2 badge style tweaks, add back v1 _set_dash_metadata for v1 editing
    
    * [dashboard] fix python linting and tests
    
    * [dashboard] lint tests
---
 superset/assets/.eslintignore                      |   1 +
 superset/assets/package.json                       |   4 +-
 .../dashboard/actions/dashboardLayout_spec.js      |   5 +-
 .../dashboard/components/DashboardBuilder_spec.jsx |  26 +-
 .../dashboard/components/DashboardGrid_spec.jsx    |  11 +-
 .../dashboard/fixtures/mockDashboardState.js       |   2 +
 .../dashboard/reducers/dashboardLayout_spec.js     |  74 ++--
 .../dashboard/reducers/dashboardState_spec.js      |   4 +-
 .../dashboard/util/isValidChild_spec.js            |   2 +-
 superset/assets/src/chart/Chart.jsx                |   4 +-
 .../src/dashboard/actions/dashboardLayout.js       |  15 +-
 .../assets/src/dashboard/actions/messageToasts.js  |  12 +-
 .../src/dashboard/components/AddSliceCard.jsx      |   2 +
 .../assets/src/dashboard/components/Controls.jsx   | 138 -------
 .../assets/src/dashboard/components/Dashboard.jsx  |  22 +-
 .../src/dashboard/components/DashboardBuilder.jsx  |  40 +-
 .../src/dashboard/components/DashboardGrid.jsx     |  73 +---
 .../assets/src/dashboard/components/Header.jsx     | 177 +++++----
 .../dashboard/components/HeaderActionsDropdown.jsx | 163 ++++++++
 .../assets/src/dashboard/components/SaveModal.jsx  |  10 +-
 .../assets/src/dashboard/components/SliceAdder.jsx |  19 +-
 superset/assets/src/dashboard/components/Toast.jsx |  15 +-
 .../src/dashboard/components/dnd/handleDrop.js     |   2 +
 .../src/dashboard/components/dnd/handleHover.js    |   2 +-
 .../components/gridComponents/ChartHolder.jsx      |   2 +-
 .../components/gridComponents/Markdown.jsx         |  49 ++-
 .../dashboard/components/gridComponents/Tab.jsx    |  23 +-
 superset/assets/src/dashboard/containers/Chart.jsx |   2 +-
 .../src/dashboard/containers/DashboardHeader.jsx   |  19 +-
 .../deprecated/PromptV2ConversionModal.jsx         | 102 +++++
 .../src/dashboard/deprecated/V2PreviewModal.jsx    | 148 +++++++
 .../src/{ => dashboard/deprecated}/chart/Chart.jsx | 150 ++++----
 .../src/dashboard/deprecated/chart/ChartBody.jsx   |  55 +++
 .../dashboard/deprecated/chart/ChartContainer.jsx  |  29 ++
 .../src/dashboard/deprecated/chart/chart.css       |   4 +
 .../src/dashboard/deprecated/chart/chartAction.js  | 195 ++++++++++
 .../src/dashboard/deprecated/chart/chartReducer.js | 158 ++++++++
 .../assets/src/dashboard/deprecated/v1/actions.js  | 127 +++++++
 .../deprecated/v1/components/CodeModal.jsx         |  48 +++
 .../deprecated/v1/components/Controls.jsx          | 214 +++++++++++
 .../deprecated/v1/components/CssEditor.jsx         |  91 +++++
 .../deprecated/v1/components/Dashboard.jsx         | 423 +++++++++++++++++++++
 .../v1/components/DashboardContainer.jsx           |  31 ++
 .../deprecated/v1/components/GridCell.jsx          | 157 ++++++++
 .../deprecated/v1/components/GridLayout.jsx        | 198 ++++++++++
 .../dashboard/deprecated/v1/components/Header.jsx  | 184 +++++++++
 .../v1/components/RefreshIntervalModal.jsx         |  64 ++++
 .../deprecated/v1/components/SaveModal.jsx         | 161 ++++++++
 .../deprecated/v1/components/SliceAdder.jsx        | 219 +++++++++++
 .../deprecated/v1/components/SliceHeader.jsx       | 194 ++++++++++
 .../assets/src/dashboard/deprecated/v1/index.jsx   |  28 ++
 .../assets/src/dashboard/deprecated/v1/reducers.js | 272 +++++++++++++
 .../src/dashboard/reducers/dashboardLayout.js      |  58 +--
 .../src/dashboard/reducers/dashboardState.js       |   2 +
 .../src/dashboard/reducers/getInitialState.js      |  27 +-
 .../dashboard/stylesheets/builder-sidepane.less    |  41 +-
 .../assets/src/dashboard/stylesheets/builder.less  |   1 -
 .../dashboard/stylesheets/components/markdown.less |   7 +-
 .../stylesheets/components/new-component.less      |   6 +-
 .../src/dashboard/stylesheets/components/tabs.less |   4 +
 .../src/dashboard/stylesheets/dashboard.less       |  74 +++-
 .../src/dashboard/stylesheets/popover-menu.less    |   1 +
 .../assets/src/dashboard/util/getDropPosition.js   |  58 ++-
 .../assets/src/dashboard/util/injectCustomCss.js   |  17 +
 superset/assets/src/dashboard/util/isValidChild.js |   2 +-
 superset/assets/src/dashboard/util/propShapes.jsx  |   1 +
 superset/assets/src/logger.js                      |  13 +
 .../assets/stylesheets/dashboard_deprecated.css    | 181 +++++++++
 superset/assets/webpack.config.js                  |   1 +
 superset/config.py                                 |   8 +
 superset/connectors/sqla/models.py                 |   4 +-
 .../superset/dashboard_v1_deprecated.html          |  10 +
 superset/views/core.py                             | 103 ++++-
 tests/dashboard_tests.py                           |  20 +-
 74 files changed, 4213 insertions(+), 596 deletions(-)

diff --git a/superset/assets/.eslintignore b/superset/assets/.eslintignore
index 7479173..61262fc 100644
--- a/superset/assets/.eslintignore
+++ b/superset/assets/.eslintignore
@@ -8,3 +8,4 @@ node_modules*/*
 stylesheets/*
 vendor/*
 docs/*
+src/dashboard/deprecated/*
diff --git a/superset/assets/package.json b/superset/assets/package.json
index 21abd17..c68e490 100644
--- a/superset/assets/package.json
+++ b/superset/assets/package.json
@@ -8,8 +8,8 @@
     "test": "spec"
   },
   "scripts": {
-    "test": "mocha --require ignore-styles --compilers js:babel-core/register --require spec/helpers/browser.js --recursive spec/**/**/*_spec.*",
-    "cover": "babel-node node_modules/.bin/babel-istanbul cover _mocha -- --require ignore-styles spec/helpers/browser.js --recursive spec/**/*_spec.*",
+    "test": "mocha --require ignore-styles --compilers js:babel-core/register --require spec/helpers/browser.js 'spec/**/*_spec.*'",
+    "cover": "babel-node node_modules/.bin/babel-istanbul cover _mocha -- --require ignore-styles spec/helpers/browser.js 'spec/**/*_spec.*'",
     "dev": "NODE_ENV=dev webpack --watch --colors --progress --debug --output-pathinfo --devtool eval-cheap-source-map",
     "dev-slow": "NODE_ENV=dev webpack --watch --colors --progress --debug --output-pathinfo --devtool inline-source-map",
     "dev-fast": "echo 'dev-fast in now replaced by dev'",
diff --git a/superset/assets/spec/javascripts/dashboard/actions/dashboardLayout_spec.js b/superset/assets/spec/javascripts/dashboard/actions/dashboardLayout_spec.js
index 0c4fe12..84f0856 100644
--- a/superset/assets/spec/javascripts/dashboard/actions/dashboardLayout_spec.js
+++ b/superset/assets/spec/javascripts/dashboard/actions/dashboardLayout_spec.js
@@ -138,7 +138,6 @@ describe('dashboardLayout actions', () => {
     });
   });
 
-  // describe('createComponent', () => {});
   describe('createTopLevelTabs', () => {
     it('should dispatch a createTopLevelTabs action', () => {
       const { getState, dispatch } = setup({
@@ -282,7 +281,7 @@ describe('dashboardLayout actions', () => {
 
     it('should move a component if the component is not new', () => {
       const { getState, dispatch } = setup({
-        dashboardLayout: { present: { id: { type: ROW_TYPE } } },
+        dashboardLayout: { present: { id: { type: ROW_TYPE, children: [] } } },
       });
       const dropResult = {
         source: { id: 'id', index: 0, type: ROW_TYPE },
@@ -324,7 +323,7 @@ describe('dashboardLayout actions', () => {
       );
     });
 
-    it('should delete the parent Tabs if the moved Tab was the only child', () => {
+    it('should delete a parent Row or Tabs if the moved child was the only child', () => {
       const { getState, dispatch } = setup({
         dashboardLayout: {
           present: {
diff --git a/superset/assets/spec/javascripts/dashboard/components/DashboardBuilder_spec.jsx b/superset/assets/spec/javascripts/dashboard/components/DashboardBuilder_spec.jsx
index 6b5d051..4c3185f 100644
--- a/superset/assets/spec/javascripts/dashboard/components/DashboardBuilder_spec.jsx
+++ b/superset/assets/spec/javascripts/dashboard/components/DashboardBuilder_spec.jsx
@@ -13,8 +13,7 @@ import DashboardBuilder from '../../../../src/dashboard/components/DashboardBuil
 import DashboardComponent from '../../../../src/dashboard/containers/DashboardComponent';
 import DashboardHeader from '../../../../src/dashboard/containers/DashboardHeader';
 import DashboardGrid from '../../../../src/dashboard/containers/DashboardGrid';
-import DragDroppable from '../../../../src/dashboard/components/dnd/DragDroppable';
-
+import WithDragDropContext from '../helpers/WithDragDropContext';
 import {
   dashboardLayout as undoableDashboardLayout,
   dashboardLayoutWithTabs as undoableDashboardLayoutWithTabs,
@@ -32,12 +31,17 @@ describe('DashboardBuilder', () => {
     editMode: false,
     showBuilderPane: false,
     handleComponentDrop() {},
+    toggleBuilderPane() {},
   };
 
   function setup(overrideProps, useProvider = false, store = mockStore) {
     const builder = <DashboardBuilder {...props} {...overrideProps} />;
     return useProvider
-      ? mount(<Provider store={store}>{builder}</Provider>)
+      ? mount(
+          <Provider store={store}>
+            <WithDragDropContext>{builder}</WithDragDropContext>
+          </Provider>,
+        )
       : shallow(builder);
   }
 
@@ -56,23 +60,11 @@ describe('DashboardBuilder', () => {
     );
   });
 
-  it('should render a DashboardHeader', () => {
-    const wrapper = setup();
+  it('should render a DragDroppable DashboardHeader', () => {
+    const wrapper = setup(null, true);
     expect(wrapper.find(DashboardHeader)).to.have.length(1);
   });
 
-  it('should render a DragDroppable DashboardHeader if editMode=true and no top-level Tabs exist', () => {
-    const withoutTabs = setup();
-    const withoutTabsEditMode = setup({ editMode: true });
-    const withTabs = setup({
-      dashboardLayout: layoutWithTabs,
-    });
-
-    expect(withoutTabs.find(DragDroppable)).to.have.length(0);
-    expect(withoutTabsEditMode.find(DragDroppable)).to.have.length(1);
-    expect(withTabs.find(DragDroppable)).to.have.length(0);
-  });
-
   it('should render a Sticky top-level Tabs if the dashboard has tabs', () => {
     const wrapper = setup(
       { dashboardLayout: layoutWithTabs },
diff --git a/superset/assets/spec/javascripts/dashboard/components/DashboardGrid_spec.jsx b/superset/assets/spec/javascripts/dashboard/components/DashboardGrid_spec.jsx
index 7e9de51..3121e7e 100644
--- a/superset/assets/spec/javascripts/dashboard/components/DashboardGrid_spec.jsx
+++ b/superset/assets/spec/javascripts/dashboard/components/DashboardGrid_spec.jsx
@@ -42,9 +42,14 @@ describe('DashboardGrid', () => {
     expect(wrapper.find(DashboardComponent)).to.have.length(2);
   });
 
-  it('should render two empty DragDroppables targets when editMode=true', () => {
-    const wrapper = setup({ editMode: true });
-    expect(wrapper.find(DragDroppable)).to.have.length(2);
+  it('should render an empty DragDroppables target when the gridComponent has no children', () => {
+    const withChildren = setup({ editMode: true });
+    const withoutChildren = setup({
+      editMode: true,
+      gridComponent: { ...props.gridComponent, children: [] },
+    });
+    expect(withChildren.find(DragDroppable)).to.have.length(0);
+    expect(withoutChildren.find(DragDroppable)).to.have.length(1);
   });
 
   it('should render grid column guides when resizing', () => {
diff --git a/superset/assets/spec/javascripts/dashboard/fixtures/mockDashboardState.js b/superset/assets/spec/javascripts/dashboard/fixtures/mockDashboardState.js
index 9d05344..fd640d1 100644
--- a/superset/assets/spec/javascripts/dashboard/fixtures/mockDashboardState.js
+++ b/superset/assets/spec/javascripts/dashboard/fixtures/mockDashboardState.js
@@ -10,4 +10,6 @@ export default {
   hasUnsavedChanges: false,
   maxUndoHistoryExceeded: false,
   isStarred: true,
+  css: '',
+  isV2Preview: false, // @TODO remove upon v1 deprecation
 };
diff --git a/superset/assets/spec/javascripts/dashboard/reducers/dashboardLayout_spec.js b/superset/assets/spec/javascripts/dashboard/reducers/dashboardLayout_spec.js
index cbe1729..dd933ac 100644
--- a/superset/assets/spec/javascripts/dashboard/reducers/dashboardLayout_spec.js
+++ b/superset/assets/spec/javascripts/dashboard/reducers/dashboardLayout_spec.js
@@ -14,7 +14,6 @@ import {
 
 import {
   CHART_TYPE,
-  COLUMN_TYPE,
   DASHBOARD_GRID_TYPE,
   DASHBOARD_ROOT_TYPE,
   ROW_TYPE,
@@ -25,7 +24,6 @@ import {
 import {
   DASHBOARD_ROOT_ID,
   DASHBOARD_GRID_ID,
-  GRID_MIN_COLUMN_COUNT,
   NEW_COMPONENTS_SOURCE_ID,
   NEW_TABS_ID,
   NEW_ROW_ID,
@@ -54,6 +52,7 @@ describe('dashboardLayout reducer', () => {
           },
           parentId: {
             id: 'parentId',
+            type: ROW_TYPE,
             children: ['toDelete', 'anotherId'],
           },
         },
@@ -66,6 +65,42 @@ describe('dashboardLayout reducer', () => {
       parentId: {
         id: 'parentId',
         children: ['anotherId'],
+        type: ROW_TYPE,
+      },
+    });
+  });
+
+  it('should delete a parent if the parent was a row and no longer has children', () => {
+    expect(
+      layoutReducer(
+        {
+          grandparentId: {
+            id: 'grandparentId',
+            children: ['parentId'],
+          },
+          parentId: {
+            id: 'parentId',
+            type: ROW_TYPE,
+            children: ['toDelete'],
+          },
+          toDelete: {
+            id: 'toDelete',
+            children: ['child1'],
+          },
+          child1: {
+            id: 'child1',
+            children: [],
+          },
+        },
+        {
+          type: DELETE_COMPONENT,
+          payload: { id: 'toDelete', parentId: 'parentId' },
+        },
+      ),
+    ).to.deep.equal({
+      grandparentId: {
+        id: 'grandparentId',
+        children: [],
       },
     });
   });
@@ -170,41 +205,6 @@ describe('dashboardLayout reducer', () => {
     });
   });
 
-  it('should set the width of a moved component with column type parent to the minimum width', () => {
-    const layout = {
-      source: {
-        id: 'source',
-        type: ROW_TYPE,
-        children: ['dontMove', 'toMove'],
-      },
-      destination: {
-        id: 'destination',
-        type: COLUMN_TYPE,
-        children: [],
-        meta: { width: 100 },
-      },
-      toMove: {
-        id: 'toMove',
-        type: CHART_TYPE,
-        children: [],
-        meta: { width: 1001 },
-      },
-    };
-
-    const dropResult = {
-      source: { id: 'source', type: ROW_TYPE, index: 1 },
-      destination: { id: 'destination', type: COLUMN_TYPE, index: 0 },
-      dragging: { id: 'toMove', type: CHART_TYPE },
-    };
-
-    const result = layoutReducer(layout, {
-      type: MOVE_COMPONENT,
-      payload: { dropResult },
-    });
-
-    expect(result.toMove.meta.width).to.equal(GRID_MIN_COLUMN_COUNT);
-  });
-
   it('should wrap a moved component in a row if need be', () => {
     const layout = {
       source: {
diff --git a/superset/assets/spec/javascripts/dashboard/reducers/dashboardState_spec.js b/superset/assets/spec/javascripts/dashboard/reducers/dashboardState_spec.js
index 89c4ffe..f8095cd 100644
--- a/superset/assets/spec/javascripts/dashboard/reducers/dashboardState_spec.js
+++ b/superset/assets/spec/javascripts/dashboard/reducers/dashboardState_spec.js
@@ -128,12 +128,14 @@ describe('dashboardState reducer', () => {
     });
   });
 
-  it('should set unsaved changes and max undo history to false on save', () => {
+  it('should set unsaved changes, max undo history, and editMode to false on save', () => {
     expect(
       dashboardStateReducer({ hasUnsavedChanges: true }, { type: ON_SAVE }),
     ).to.deep.equal({
       hasUnsavedChanges: false,
       maxUndoHistoryExceeded: false,
+      editMode: false,
+      isV2Preview: false, // @TODO remove upon v1 deprecation
     });
   });
 
diff --git a/superset/assets/spec/javascripts/dashboard/util/isValidChild_spec.js b/superset/assets/spec/javascripts/dashboard/util/isValidChild_spec.js
index ec57494..3563059 100644
--- a/superset/assets/spec/javascripts/dashboard/util/isValidChild_spec.js
+++ b/superset/assets/spec/javascripts/dashboard/util/isValidChild_spec.js
@@ -108,7 +108,7 @@ describe('isValidChild', () => {
       [ROOT, [MARKDOWN]],
       [ROOT, GRID, [TAB]],
       [ROOT, GRID, TABS, [ROW]],
-      [ROOT, GRID, TABS, TAB, [TABS]],
+      // [ROOT, GRID, TABS, TAB, [TABS]], // @TODO this needs to be fixed
       [ROOT, GRID, ROW, [TABS]],
       [ROOT, GRID, ROW, [TAB]],
       [ROOT, GRID, ROW, [DIVIDER]],
diff --git a/superset/assets/src/chart/Chart.jsx b/superset/assets/src/chart/Chart.jsx
index 060249f..1718fc7 100644
--- a/superset/assets/src/chart/Chart.jsx
+++ b/superset/assets/src/chart/Chart.jsx
@@ -190,8 +190,8 @@ class Chart extends React.PureComponent {
         this.props.actions.chartRenderingSucceeded(chartId);
       }
       Logger.append(LOG_ACTIONS_RENDER_CHART, {
-        label: 'slice_' + chartId,
-        vis_type: vizType,
+        slice_id: 'slice_' + chartId,
+        viz_type: vizType,
         start_offset: renderStart,
         duration: Logger.getTimestamp() - renderStart,
       });
diff --git a/superset/assets/src/dashboard/actions/dashboardLayout.js b/superset/assets/src/dashboard/actions/dashboardLayout.js
index d210ee6..c4908b0 100644
--- a/superset/assets/src/dashboard/actions/dashboardLayout.js
+++ b/superset/assets/src/dashboard/actions/dashboardLayout.js
@@ -2,7 +2,12 @@ import { ActionCreators as UndoActionCreators } from 'redux-undo';
 
 import { addInfoToast } from './messageToasts';
 import { setUnsavedChanges } from './dashboardState';
-import { CHART_TYPE, MARKDOWN_TYPE, TABS_TYPE } from '../util/componentTypes';
+import {
+  CHART_TYPE,
+  MARKDOWN_TYPE,
+  TABS_TYPE,
+  ROW_TYPE,
+} from '../util/componentTypes';
 import {
   DASHBOARD_ROOT_ID,
   NEW_COMPONENTS_SOURCE_ID,
@@ -155,8 +160,7 @@ export function handleComponentDrop(dropResult) {
     if (overflowsParent) {
       return dispatch(
         addInfoToast(
-          `Parent does not have enough space for this component.
-         Try decreasing its width or add it to a new row.`,
+          `Parent does not have enough space for this component. Try decreasing its width or add it to a new row.`,
         ),
       );
     }
@@ -180,12 +184,13 @@ export function handleComponentDrop(dropResult) {
 
     const { dashboardLayout: undoableLayout } = getState();
 
-    // if we moved a Tab and the parent Tabs no longer has children, delete it.
+    // if we moved a child from a Tab or Row parent and it was the only child, delete the parent.
     if (!isNewComponent) {
       const { present: layout } = undoableLayout;
       const sourceComponent = layout[source.id];
       if (
-        sourceComponent.type === TABS_TYPE &&
+        (sourceComponent.type === TABS_TYPE ||
+          sourceComponent.type === ROW_TYPE) &&
         sourceComponent.children.length === 0
       ) {
         const parentId = findParentId({
diff --git a/superset/assets/src/dashboard/actions/messageToasts.js b/superset/assets/src/dashboard/actions/messageToasts.js
index fde02c4..e5c04e6 100644
--- a/superset/assets/src/dashboard/actions/messageToasts.js
+++ b/superset/assets/src/dashboard/actions/messageToasts.js
@@ -12,13 +12,14 @@ function getToastUuid(type) {
 }
 
 export const ADD_TOAST = 'ADD_TOAST';
-export function addToast({ toastType, text }) {
+export function addToast({ toastType, text, duration }) {
   return {
     type: ADD_TOAST,
     payload: {
       id: getToastUuid(toastType),
       toastType,
       text,
+      duration,
     },
   };
 }
@@ -36,17 +37,20 @@ export function removeToast(id) {
 // Different types of toasts
 export const ADD_INFO_TOAST = 'ADD_INFO_TOAST';
 export function addInfoToast(text) {
-  return dispatch => dispatch(addToast({ text, toastType: INFO_TOAST }));
+  return dispatch =>
+    dispatch(addToast({ text, toastType: INFO_TOAST, duration: 4000 }));
 }
 
 export const ADD_SUCCESS_TOAST = 'ADD_SUCCESS_TOAST';
 export function addSuccessToast(text) {
-  return dispatch => dispatch(addToast({ text, toastType: SUCCESS_TOAST }));
+  return dispatch =>
+    dispatch(addToast({ text, toastType: SUCCESS_TOAST, duration: 4000 }));
 }
 
 export const ADD_WARNING_TOAST = 'ADD_WARNING_TOAST';
 export function addWarningToast(text) {
-  return dispatch => dispatch(addToast({ text, toastType: WARNING_TOAST }));
+  return dispatch =>
+    dispatch(addToast({ text, toastType: WARNING_TOAST, duration: 4000 }));
 }
 
 export const ADD_DANGER_TOAST = 'ADD_DANGER_TOAST';
diff --git a/superset/assets/src/dashboard/components/AddSliceCard.jsx b/superset/assets/src/dashboard/components/AddSliceCard.jsx
index 7fd9ba4..c8266ad 100644
--- a/superset/assets/src/dashboard/components/AddSliceCard.jsx
+++ b/superset/assets/src/dashboard/components/AddSliceCard.jsx
@@ -1,6 +1,7 @@
 import cx from 'classnames';
 import React from 'react';
 import PropTypes from 'prop-types';
+import { t } from '../../locales';
 
 const propTypes = {
   datasourceLink: PropTypes.string,
@@ -49,6 +50,7 @@ function AddSliceCard({
           </div>
         </div>
       </div>
+      {isSelected && <div className="is-added-label">{t('Added')}</div>}
     </div>
   );
 }
diff --git a/superset/assets/src/dashboard/components/Controls.jsx b/superset/assets/src/dashboard/components/Controls.jsx
deleted file mode 100644
index 9d54b09..0000000
--- a/superset/assets/src/dashboard/components/Controls.jsx
+++ /dev/null
@@ -1,138 +0,0 @@
-/* global window */
-import React from 'react';
-import PropTypes from 'prop-types';
-import $ from 'jquery';
-import { DropdownButton, MenuItem } from 'react-bootstrap';
-
-import CssEditor from './CssEditor';
-import RefreshIntervalModal from './RefreshIntervalModal';
-import { t } from '../../locales';
-
-function updateDom(css) {
-  const className = 'CssEditor-css';
-  const head = document.head || document.getElementsByTagName('head')[0];
-  let style = document.querySelector(`.${className}`);
-
-  if (!style) {
-    style = document.createElement('style');
-    style.className = className;
-    style.type = 'text/css';
-    head.appendChild(style);
-  }
-  if (style.styleSheet) {
-    style.styleSheet.cssText = css;
-  } else {
-    style.innerHTML = css;
-  }
-}
-
-const propTypes = {
-  addSuccessToast: PropTypes.func.isRequired,
-  addDangerToast: PropTypes.func.isRequired,
-  dashboardInfo: PropTypes.object.isRequired,
-  dashboardTitle: PropTypes.string.isRequired,
-  css: PropTypes.string.isRequired,
-  slices: PropTypes.array,
-  onChange: PropTypes.func.isRequired,
-  updateCss: PropTypes.func.isRequired,
-  forceRefreshAllCharts: PropTypes.func.isRequired,
-  startPeriodicRender: PropTypes.func.isRequired,
-  editMode: PropTypes.bool,
-};
-
-const defaultProps = {
-  editMode: false,
-  slices: [],
-};
-
-class Controls extends React.PureComponent {
-  constructor(props) {
-    super(props);
-    this.state = {
-      css: props.css,
-      cssTemplates: [],
-    };
-
-    this.changeCss = this.changeCss.bind(this);
-  }
-
-  componentWillMount() {
-    updateDom(this.state.css);
-
-    $.get('/csstemplateasyncmodelview/api/read', data => {
-      const cssTemplates = data.result.map(row => ({
-        value: row.template_name,
-        css: row.css,
-        label: row.template_name,
-      }));
-      this.setState({ cssTemplates });
-    });
-  }
-
-  changeCss(css) {
-    this.setState({ css }, () => {
-      updateDom(css);
-    });
-    this.props.onChange();
-    this.props.updateCss(css);
-  }
-
-  render() {
-    const {
-      dashboardTitle,
-      startPeriodicRender,
-      forceRefreshAllCharts,
-      editMode,
-    } = this.props;
-
-    const emailBody = t('Checkout this dashboard: %s', window.location.href);
-    const emailLink =
-      'mailto:?Subject=Superset%20Dashboard%20' +
-      `${dashboardTitle}&Body=${emailBody}`;
-
-    return (
-      <span>
-        <DropdownButton
-          title="Actions"
-          bsSize="small"
-          id="bg-nested-dropdown"
-          pullRight
-        >
-          <MenuItem onClick={forceRefreshAllCharts}>
-            {t('Force refresh dashboard')}
-          </MenuItem>
-          <RefreshIntervalModal
-            onChange={refreshInterval =>
-              startPeriodicRender(refreshInterval * 1000)
-            }
-            triggerNode={<span>{t('Set auto-refresh interval')}</span>}
-          />
-          {editMode && (
-            <MenuItem
-              target="_blank"
-              href={`/dashboardmodelview/edit/${this.props.dashboardInfo.id}`}
-            >
-              {t('Edit dashboard metadata')}
-            </MenuItem>
-          )}
-          {editMode && (
-            <MenuItem href={emailLink}>{t('Email dashboard link')}</MenuItem>
-          )}
-          {editMode && (
-            <CssEditor
-              triggerNode={<span>{t('Edit CSS')}</span>}
-              initialCss={this.state.css}
-              templates={this.state.cssTemplates}
-              onChange={this.changeCss}
-            />
-          )}
-        </DropdownButton>
-      </span>
-    );
-  }
-}
-
-Controls.propTypes = propTypes;
-Controls.defaultProps = defaultProps;
-
-export default Controls;
diff --git a/superset/assets/src/dashboard/components/Dashboard.jsx b/superset/assets/src/dashboard/components/Dashboard.jsx
index 62bcbb5..99e93aa 100644
--- a/superset/assets/src/dashboard/components/Dashboard.jsx
+++ b/superset/assets/src/dashboard/components/Dashboard.jsx
@@ -86,6 +86,9 @@ class Dashboard extends React.PureComponent {
 
   componentWillReceiveProps(nextProps) {
     if (!nextProps.dashboardState.editMode) {
+      const version = nextProps.dashboardState.isV2Preview
+        ? 'v2-preview'
+        : 'v2';
       // log pane loads
       const loadedPaneIds = [];
       const allPanesDidLoad = Object.entries(nextProps.loadStats).every(
@@ -101,6 +104,7 @@ class Dashboard extends React.PureComponent {
             Logger.append(LOG_ACTIONS_LOAD_DASHBOARD_PANE, {
               ...restStats,
               duration,
+              version,
             });
 
             if (!this.isFirstLoad) {
@@ -118,6 +122,7 @@ class Dashboard extends React.PureComponent {
         Logger.append(LOG_ACTIONS_FIRST_DASHBOARD_LOAD, {
           pane_ids: loadedPaneIds,
           duration: new Date().getTime() - this.ts_mount,
+          version,
         });
         Logger.send(this.actionLog);
         this.isFirstLoad = false;
@@ -128,25 +133,20 @@ class Dashboard extends React.PureComponent {
     const nextChartIds = getChartIdsFromLayout(nextProps.layout);
 
     if (currentChartIds.length < nextChartIds.length) {
-      // adding new chart
       const newChartIds = nextChartIds.filter(
         key => currentChartIds.indexOf(key) === -1,
       );
-      if (newChartIds.length) {
-        newChartIds.forEach(newChartId =>
-          this.props.actions.addSliceToDashboard(newChartId),
-        );
-      }
+      newChartIds.forEach(newChartId =>
+        this.props.actions.addSliceToDashboard(newChartId),
+      );
     } else if (currentChartIds.length > nextChartIds.length) {
       // remove chart
       const removedChartIds = currentChartIds.filter(
         key => nextChartIds.indexOf(key) === -1,
       );
-      if (removedChartIds.length) {
-        removedChartIds.forEach(removedChartId =>
-          this.props.actions.removeSliceFromDashboard(removedChartId),
-        );
-      }
+      removedChartIds.forEach(removedChartId =>
+        this.props.actions.removeSliceFromDashboard(removedChartId),
+      );
     }
   }
 
diff --git a/superset/assets/src/dashboard/components/DashboardBuilder.jsx b/superset/assets/src/dashboard/components/DashboardBuilder.jsx
index 30e2e78..2156ed3 100644
--- a/superset/assets/src/dashboard/components/DashboardBuilder.jsx
+++ b/superset/assets/src/dashboard/components/DashboardBuilder.jsx
@@ -96,26 +96,24 @@ class DashboardBuilder extends React.Component {
       <StickyContainer
         className={cx('dashboard', editMode && 'dashboard--editing')}
       >
-        {topLevelTabs || !editMode ? ( // you cannot drop on/displace tabs if they already exist
-          <DashboardHeader />
-        ) : (
-          <DragDroppable
-            component={dashboardRoot}
-            parentComponent={null}
-            depth={DASHBOARD_ROOT_DEPTH}
-            index={0}
-            orientation="column"
-            onDrop={handleComponentDrop}
-            editMode
-          >
-            {({ dropIndicatorProps }) => (
-              <div>
-                <DashboardHeader />
-                {dropIndicatorProps && <div {...dropIndicatorProps} />}
-              </div>
-            )}
-          </DragDroppable>
-        )}
+        <DragDroppable
+          component={dashboardRoot}
+          parentComponent={null}
+          depth={DASHBOARD_ROOT_DEPTH}
+          index={0}
+          orientation="column"
+          onDrop={handleComponentDrop}
+          editMode
+          // you cannot drop on/displace tabs if they already exist
+          disableDragdrop={!editMode || topLevelTabs}
+        >
+          {({ dropIndicatorProps }) => (
+            <div>
+              <DashboardHeader />
+              {dropIndicatorProps && <div {...dropIndicatorProps} />}
+            </div>
+          )}
+        </DragDroppable>
 
         {topLevelTabs && (
           <Sticky topOffset={50}>
@@ -175,7 +173,7 @@ class DashboardBuilder extends React.Component {
                         <DashboardGrid
                           gridComponent={dashboardLayout[id]}
                           // see isValidChild for why tabs do not increment the depth of their children
-                          depth={DASHBOARD_ROOT_DEPTH + (topLevelTabs ? 0 : 1)}
+                          depth={DASHBOARD_ROOT_DEPTH + 1} // (topLevelTabs ? 0 : 1)}
                           width={width}
                         />
                       </TabPane>
diff --git a/superset/assets/src/dashboard/components/DashboardGrid.jsx b/superset/assets/src/dashboard/components/DashboardGrid.jsx
index 77503bb..4689051 100644
--- a/superset/assets/src/dashboard/components/DashboardGrid.jsx
+++ b/superset/assets/src/dashboard/components/DashboardGrid.jsx
@@ -26,7 +26,6 @@ class DashboardGrid extends React.PureComponent {
       rowGuideTop: null,
     };
 
-    this.handleTopDropTargetDrop = this.handleTopDropTargetDrop.bind(this);
     this.handleResizeStart = this.handleResizeStart.bind(this);
     this.handleResize = this.handleResize.bind(this);
     this.handleResizeStop = this.handleResizeStop.bind(this);
@@ -76,19 +75,6 @@ class DashboardGrid extends React.PureComponent {
     }));
   }
 
-  handleTopDropTargetDrop(dropResult) {
-    if (dropResult) {
-      this.props.handleComponentDrop({
-        ...dropResult,
-        destination: {
-          ...dropResult.destination,
-          // force appending as the first child if top drop target
-          index: 0,
-        },
-      });
-    }
-  }
-
   render() {
     const {
       gridComponent,
@@ -107,26 +93,6 @@ class DashboardGrid extends React.PureComponent {
     return width < 100 ? null : (
       <div className="dashboard-grid" ref={this.setGridRef}>
         <div className="grid-content">
-          {/* empty drop target makes top droppable */}
-          {editMode && (
-            <DragDroppable
-              component={gridComponent}
-              depth={depth}
-              parentComponent={null}
-              index={0}
-              orientation="column"
-              onDrop={this.handleTopDropTargetDrop}
-              className="empty-grid-droptarget--top"
-              editMode
-            >
-              {({ dropIndicatorProps }) =>
-                dropIndicatorProps && (
-                  <div className="drop-indicator drop-indicator--bottom" />
-                )
-              }
-            </DragDroppable>
-          )}
-
           {gridComponent.children.map((id, index) => (
             <DashboardComponent
               key={id}
@@ -142,25 +108,26 @@ class DashboardGrid extends React.PureComponent {
             />
           ))}
 
-          {/* empty drop target makes bottom droppable */}
-          {editMode && (
-            <DragDroppable
-              component={gridComponent}
-              depth={depth}
-              parentComponent={null}
-              index={gridComponent.children.length}
-              orientation="column"
-              onDrop={handleComponentDrop}
-              className="empty-grid-droptarget--bottom"
-              editMode
-            >
-              {({ dropIndicatorProps }) =>
-                dropIndicatorProps && (
-                  <div className="drop-indicator drop-indicator--top" />
-                )
-              }
-            </DragDroppable>
-          )}
+          {/* make the grid droppable in the case that there are no children */}
+          {editMode &&
+            gridComponent.children.length === 0 && (
+              <DragDroppable
+                component={gridComponent}
+                depth={depth}
+                parentComponent={null}
+                index={gridComponent.children.length}
+                orientation="column"
+                onDrop={handleComponentDrop}
+                className="empty-grid-droptarget--bottom"
+                editMode
+              >
+                {({ dropIndicatorProps }) =>
+                  dropIndicatorProps && (
+                    <div className="drop-indicator drop-indicator--top" />
+                  )
+                }
+              </DragDroppable>
+            )}
 
           {isResizing &&
             Array(GRID_COLUMN_COUNT)
diff --git a/superset/assets/src/dashboard/components/Header.jsx b/superset/assets/src/dashboard/components/Header.jsx
index 31bd08c..5fa4afe 100644
--- a/superset/assets/src/dashboard/components/Header.jsx
+++ b/superset/assets/src/dashboard/components/Header.jsx
@@ -1,25 +1,17 @@
 /* eslint-env browser */
 import React from 'react';
 import PropTypes from 'prop-types';
-import {
-  DropdownButton,
-  MenuItem,
-  ButtonGroup,
-  ButtonToolbar,
-} from 'react-bootstrap';
+import { ButtonGroup, ButtonToolbar } from 'react-bootstrap';
 
-import Controls from './Controls';
+import HeaderActionsDropdown from './HeaderActionsDropdown';
 import EditableTitle from '../../components/EditableTitle';
 import Button from '../../components/Button';
 import FaveStar from '../../components/FaveStar';
-import SaveModal from './SaveModal';
+import V2PreviewModal from '../deprecated/V2PreviewModal';
+
 import { chartPropShape } from '../util/propShapes';
 import { t } from '../../locales';
-import {
-  UNDO_LIMIT,
-  SAVE_TYPE_NEWDASHBOARD,
-  SAVE_TYPE_OVERWRITE,
-} from '../util/constants';
+import { UNDO_LIMIT, SAVE_TYPE_OVERWRITE } from '../util/constants';
 
 const propTypes = {
   addSuccessToast: PropTypes.func.isRequired,
@@ -40,6 +32,7 @@ const propTypes = {
   startPeriodicRender: PropTypes.func.isRequired,
   updateDashboardTitle: PropTypes.func.isRequired,
   editMode: PropTypes.bool.isRequired,
+  isV2Preview: PropTypes.bool.isRequired,
   setEditMode: PropTypes.func.isRequired,
   showBuilderPane: PropTypes.bool.isRequired,
   toggleBuilderPane: PropTypes.func.isRequired,
@@ -65,12 +58,14 @@ class Header extends React.PureComponent {
     super(props);
     this.state = {
       didNotifyMaxUndoHistoryToast: false,
+      showV2PreviewModal: props.isV2Preview,
     };
 
     this.handleChangeText = this.handleChangeText.bind(this);
     this.toggleEditMode = this.toggleEditMode.bind(this);
     this.forceRefresh = this.forceRefresh.bind(this);
     this.overwriteDashboard = this.overwriteDashboard.bind(this);
+    this.toggleShowV2PreviewModal = this.toggleShowV2PreviewModal.bind(this);
   }
 
   componentWillReceiveProps(nextProps) {
@@ -105,6 +100,10 @@ class Header extends React.PureComponent {
     this.props.setEditMode(!this.props.editMode);
   }
 
+  toggleShowV2PreviewModal() {
+    this.setState({ showV2PreviewModal: !this.state.showV2PreviewModal });
+  }
+
   overwriteDashboard() {
     const {
       dashboardTitle,
@@ -133,6 +132,7 @@ class Header extends React.PureComponent {
       filters,
       expandedSlices,
       css,
+      isV2Preview,
       onUndo,
       onRedo,
       undoLength,
@@ -148,6 +148,7 @@ class Header extends React.PureComponent {
 
     const userCanEdit = dashboardInfo.dash_edit_perm;
     const userCanSaveAs = dashboardInfo.dash_save_perm;
+    const popButton = hasUnsavedChanges || isV2Preview;
 
     return (
       <div className="dashboard-header">
@@ -158,7 +159,7 @@ class Header extends React.PureComponent {
             onSaveTitle={this.handleChangeText}
             showTooltip={false}
           />
-          <span className="favstar m-l-5">
+          <span className="favstar">
             <FaveStar
               itemId={dashboardInfo.id}
               fetchFaveStar={this.props.fetchFaveStar}
@@ -166,7 +167,22 @@ class Header extends React.PureComponent {
               isStarred={this.props.isStarred}
             />
           </span>
+          {isV2Preview && (
+            <div
+              role="none"
+              className="v2-preview-badge"
+              onClick={this.toggleShowV2PreviewModal}
+            >
+              {t('v2 Preview')}
+              <span className="fa fa-info-circle m-l-5" />
+            </div>
+          )}
+          {isV2Preview &&
+            this.state.showV2PreviewModal && (
+              <V2PreviewModal onClose={this.toggleShowV2PreviewModal} />
+            )}
         </div>
+
         <ButtonToolbar>
           {userCanSaveAs && (
             <ButtonGroup>
@@ -193,76 +209,83 @@ class Header extends React.PureComponent {
               {editMode && (
                 <Button bsSize="small" onClick={this.props.toggleBuilderPane}>
                   {showBuilderPane
-                    ? t('Hide builder pane')
+                    ? t('Hide components')
                     : t('Insert components')}
                 </Button>
               )}
 
-              {!hasUnsavedChanges ? (
-                <Button
-                  bsSize="small"
-                  onClick={this.toggleEditMode}
-                  bsStyle={hasUnsavedChanges ? 'primary' : undefined}
-                  disabled={!userCanEdit}
-                >
-                  {editMode ? t('Switch to view mode') : t('Edit dashboard')}
-                </Button>
-              ) : (
-                <Button
-                  bsSize="small"
-                  bsStyle={hasUnsavedChanges ? 'primary' : undefined}
-                  onClick={this.overwriteDashboard}
-                >
-                  {t('Save changes')}
-                </Button>
-              )}
-              <DropdownButton
-                title=""
-                id="save-dash-split-button"
-                bsStyle={hasUnsavedChanges ? 'primary' : undefined}
-                bsSize="small"
-                pullRight
-              >
-                <SaveModal
-                  addSuccessToast={this.props.addSuccessToast}
-                  addDangerToast={this.props.addDangerToast}
-                  dashboardId={dashboardInfo.id}
-                  dashboardTitle={dashboardTitle}
-                  saveType={SAVE_TYPE_NEWDASHBOARD}
-                  layout={layout}
-                  filters={filters}
-                  expandedSlices={expandedSlices}
-                  css={css}
-                  onSave={onSave}
-                  isMenuItem
-                  triggerNode={<span>{t('Save as')}</span>}
-                  canOverwrite={userCanEdit}
-                />
-                {hasUnsavedChanges && (
-                  <MenuItem eventKey="discard" onSelect={Header.discardChanges}>
-                    {t('Discard changes')}
-                  </MenuItem>
+              {editMode &&
+                (hasUnsavedChanges || isV2Preview) && (
+                  <Button
+                    bsSize="small"
+                    bsStyle={popButton ? 'primary' : undefined}
+                    onClick={this.overwriteDashboard}
+                  >
+                    {isV2Preview
+                      ? t('Persist as Dashboard v2')
+                      : t('Save changes')}
+                  </Button>
+                )}
+
+              {!editMode &&
+                isV2Preview && (
+                  <Button
+                    bsSize="small"
+                    onClick={this.toggleEditMode}
+                    bsStyle={popButton ? 'primary' : undefined}
+                    disabled={!userCanEdit}
+                  >
+                    {t('Edit to persist Dashboard v2')}
+                  </Button>
+                )}
+
+              {!editMode &&
+                !isV2Preview &&
+                !hasUnsavedChanges && (
+                  <Button
+                    bsSize="small"
+                    onClick={this.toggleEditMode}
+                    bsStyle={popButton ? 'primary' : undefined}
+                    disabled={!userCanEdit}
+                  >
+                    {t('Edit dashboard')}
+                  </Button>
                 )}
-              </DropdownButton>
+
+              {editMode &&
+                !isV2Preview &&
+                !hasUnsavedChanges && (
+                  <Button
+                    bsSize="small"
+                    onClick={this.toggleEditMode}
+                    bsStyle={undefined}
+                    disabled={!userCanEdit}
+                  >
+                    {t('Switch to view mode')}
+                  </Button>
+                )}
+
+              <HeaderActionsDropdown
+                addSuccessToast={this.props.addSuccessToast}
+                addDangerToast={this.props.addDangerToast}
+                dashboardId={dashboardInfo.id}
+                dashboardTitle={dashboardTitle}
+                layout={layout}
+                filters={filters}
+                expandedSlices={expandedSlices}
+                css={css}
+                onSave={onSave}
+                onChange={onChange}
+                forceRefreshAllCharts={this.forceRefresh}
+                startPeriodicRender={this.props.startPeriodicRender}
+                updateCss={updateCss}
+                editMode={editMode}
+                hasUnsavedChanges={hasUnsavedChanges}
+                userCanEdit={userCanEdit}
+                isV2Preview={isV2Preview}
+              />
             </ButtonGroup>
           )}
-
-          <Controls
-            addSuccessToast={this.props.addSuccessToast}
-            addDangerToast={this.props.addDangerToast}
-            dashboardInfo={dashboardInfo}
-            dashboardTitle={dashboardTitle}
-            layout={layout}
-            filters={filters}
-            expandedSlices={expandedSlices}
-            css={css}
-            onSave={onSave}
-            onChange={onChange}
-            forceRefreshAllCharts={this.forceRefresh}
-            startPeriodicRender={this.props.startPeriodicRender}
-            updateCss={updateCss}
-            editMode={editMode}
-          />
         </ButtonToolbar>
       </div>
     );
diff --git a/superset/assets/src/dashboard/components/HeaderActionsDropdown.jsx b/superset/assets/src/dashboard/components/HeaderActionsDropdown.jsx
new file mode 100644
index 0000000..7b8a245
--- /dev/null
+++ b/superset/assets/src/dashboard/components/HeaderActionsDropdown.jsx
@@ -0,0 +1,163 @@
+/* global window */
+import React from 'react';
+import PropTypes from 'prop-types';
+import $ from 'jquery';
+import { DropdownButton, MenuItem } from 'react-bootstrap';
+
+import CssEditor from './CssEditor';
+import RefreshIntervalModal from './RefreshIntervalModal';
+import SaveModal from './SaveModal';
+import injectCustomCss from '../util/injectCustomCss';
+import { SAVE_TYPE_NEWDASHBOARD } from '../util/constants';
+import { t } from '../../locales';
+
+const propTypes = {
+  addSuccessToast: PropTypes.func.isRequired,
+  addDangerToast: PropTypes.func.isRequired,
+  dashboardId: PropTypes.number.isRequired,
+  dashboardTitle: PropTypes.string.isRequired,
+  hasUnsavedChanges: PropTypes.bool.isRequired,
+  css: PropTypes.string.isRequired,
+  onChange: PropTypes.func.isRequired,
+  updateCss: PropTypes.func.isRequired,
+  forceRefreshAllCharts: PropTypes.func.isRequired,
+  startPeriodicRender: PropTypes.func.isRequired,
+  editMode: PropTypes.bool.isRequired,
+  userCanEdit: PropTypes.bool.isRequired,
+  layout: PropTypes.object.isRequired,
+  filters: PropTypes.object.isRequired,
+  expandedSlices: PropTypes.object.isRequired,
+  onSave: PropTypes.func.isRequired,
+  isV2Preview: PropTypes.bool.isRequired,
+};
+
+const defaultProps = {};
+
+class HeaderActionsDropdown extends React.PureComponent {
+  static discardChanges() {
+    window.location.reload();
+  }
+
+  constructor(props) {
+    super(props);
+    this.state = {
+      css: props.css,
+      cssTemplates: [],
+    };
+
+    this.changeCss = this.changeCss.bind(this);
+  }
+
+  componentWillMount() {
+    injectCustomCss(this.state.css);
+
+    $.get('/csstemplateasyncmodelview/api/read', data => {
+      const cssTemplates = data.result.map(row => ({
+        value: row.template_name,
+        css: row.css,
+        label: row.template_name,
+      }));
+      this.setState({ cssTemplates });
+    });
+  }
+
+  changeCss(css) {
+    this.setState({ css }, () => {
+      injectCustomCss(css);
+    });
+    this.props.onChange();
+    this.props.updateCss(css);
+  }
+
+  render() {
+    const {
+      dashboardTitle,
+      dashboardId,
+      startPeriodicRender,
+      forceRefreshAllCharts,
+      editMode,
+      css,
+      hasUnsavedChanges,
+      layout,
+      filters,
+      expandedSlices,
+      onSave,
+      userCanEdit,
+      isV2Preview,
+    } = this.props;
+
+    const emailBody = t('Check out this dashboard: %s', window.location.href);
+    const emailLink = `mailto:?Subject=Superset%20Dashboard%20${dashboardTitle}&Body=${emailBody}`;
+
+    return (
+      <DropdownButton
+        title=""
+        id="save-dash-split-button"
+        bsStyle={hasUnsavedChanges || isV2Preview ? 'primary' : undefined}
+        bsSize="small"
+        pullRight
+      >
+        <SaveModal
+          addSuccessToast={this.props.addSuccessToast}
+          addDangerToast={this.props.addDangerToast}
+          dashboardId={dashboardId}
+          dashboardTitle={dashboardTitle}
+          saveType={SAVE_TYPE_NEWDASHBOARD}
+          layout={layout}
+          filters={filters}
+          expandedSlices={expandedSlices}
+          css={css}
+          onSave={onSave}
+          isMenuItem
+          triggerNode={<span>{t('Save as')}</span>}
+          canOverwrite={userCanEdit}
+          isV2Preview={isV2Preview}
+        />
+        {(isV2Preview || hasUnsavedChanges) && (
+          <MenuItem
+            eventKey="discard"
+            onSelect={HeaderActionsDropdown.discardChanges}
+          >
+            {t('Discard changes')}
+          </MenuItem>
+        )}
+
+        <MenuItem divider />
+
+        <MenuItem onClick={forceRefreshAllCharts}>
+          {t('Force refresh dashboard')}
+        </MenuItem>
+        <RefreshIntervalModal
+          onChange={refreshInterval =>
+            startPeriodicRender(refreshInterval * 1000)
+          }
+          triggerNode={<span>{t('Set auto-refresh interval')}</span>}
+        />
+        {editMode && (
+          <MenuItem
+            target="_blank"
+            href={`/dashboardmodelview/edit/${dashboardId}`}
+          >
+            {t('Edit dashboard metadata')}
+          </MenuItem>
+        )}
+        {editMode && (
+          <MenuItem href={emailLink}>{t('Email dashboard link')}</MenuItem>
+        )}
+        {editMode && (
+          <CssEditor
+            triggerNode={<span>{t('Edit CSS')}</span>}
+            initialCss={this.state.css}
+            templates={this.state.cssTemplates}
+            onChange={this.changeCss}
+          />
+        )}
+      </DropdownButton>
+    );
+  }
+}
+
+HeaderActionsDropdown.propTypes = propTypes;
+HeaderActionsDropdown.defaultProps = defaultProps;
+
+export default HeaderActionsDropdown;
diff --git a/superset/assets/src/dashboard/components/SaveModal.jsx b/superset/assets/src/dashboard/components/SaveModal.jsx
index 9d63331..f5ad9d0 100644
--- a/superset/assets/src/dashboard/components/SaveModal.jsx
+++ b/superset/assets/src/dashboard/components/SaveModal.jsx
@@ -22,6 +22,7 @@ const propTypes = {
   onSave: PropTypes.func.isRequired,
   isMenuItem: PropTypes.bool,
   canOverwrite: PropTypes.bool.isRequired,
+  isV2Preview: PropTypes.bool.isRequired,
 };
 
 const defaultProps = {
@@ -82,7 +83,8 @@ class SaveModal extends React.PureComponent {
       positions,
       css,
       expanded_slices: expandedSlices,
-      dashboard_title: dashboardTitle,
+      dashboard_title:
+        saveType === SAVE_TYPE_NEWDASHBOARD ? newDashName : dashboardTitle,
       default_filters: JSON.stringify(filters),
       duplicate_slices: this.state.duplicateSlices,
     };
@@ -102,12 +104,16 @@ class SaveModal extends React.PureComponent {
   }
 
   render() {
+    const { isV2Preview } = this.props;
     return (
       <ModalTrigger
         ref={this.setModalRef}
         isMenuItem={this.props.isMenuItem}
         triggerNode={this.props.triggerNode}
-        modalTitle={t('Save Dashboard')}
+        modalTitle={t(
+          'Save Dashboard%s',
+          isV2Preview ? ' (⚠️ all saved dashboards will be V2)' : '',
+        )}
         modalBody={
           <FormGroup>
             <Radio
diff --git a/superset/assets/src/dashboard/components/SliceAdder.jsx b/superset/assets/src/dashboard/components/SliceAdder.jsx
index d8ed53e..9e68278 100644
--- a/superset/assets/src/dashboard/components/SliceAdder.jsx
+++ b/superset/assets/src/dashboard/components/SliceAdder.jsx
@@ -34,7 +34,7 @@ const defaultProps = {
 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: 'viz_type', label: 'Vis type' },
   { key: 'datasource_name', label: 'Datasource' },
   { key: 'changed_on', label: 'Recent' },
 ];
@@ -187,23 +187,24 @@ class SliceAdder extends React.Component {
     return (
       <div className="slice-adder-container">
         <div className="controls">
+          <SearchInput
+            placeholder="Filter your charts"
+            className="search-input"
+            onChange={this.searchUpdated}
+            onKeyPress={this.handleKeyPress}
+          />
+
           <DropdownButton
-            title={KEYS_TO_SORT[this.state.sortBy].label}
+            title={`Sort by ${KEYS_TO_SORT[this.state.sortBy].label}`}
             onSelect={this.handleSelect}
             id="slice-adder-sortby"
           >
             {KEYS_TO_SORT.map((item, index) => (
               <MenuItem key={item.key} eventKey={index}>
-                {item.label}
+                Sort by {item.label}
               </MenuItem>
             ))}
           </DropdownButton>
-
-          <SearchInput
-            className="search-input"
-            onChange={this.searchUpdated}
-            onKeyPress={this.handleKeyPress}
-          />
         </div>
 
         {this.props.isLoading && (
diff --git a/superset/assets/src/dashboard/components/Toast.jsx b/superset/assets/src/dashboard/components/Toast.jsx
index 3c5a3ca..a2b5f0a 100644
--- a/superset/assets/src/dashboard/components/Toast.jsx
+++ b/superset/assets/src/dashboard/components/Toast.jsx
@@ -14,14 +14,9 @@ import {
 const propTypes = {
   toast: toastShape.isRequired,
   onCloseToast: PropTypes.func.isRequired,
-  delay: PropTypes.number,
-  duration: PropTypes.number, // if duration is >0, the toast will close on its own
 };
 
-const defaultProps = {
-  delay: 0,
-  duration: 0,
-};
+const defaultProps = {};
 
 class Toast extends React.Component {
   constructor(props) {
@@ -35,12 +30,12 @@ class Toast extends React.Component {
   }
 
   componentDidMount() {
-    const { delay, duration } = this.props;
+    const { toast } = this.props;
 
-    setTimeout(this.showToast, delay);
+    setTimeout(this.showToast);
 
-    if (duration > 0) {
-      this.hideTimer = setTimeout(this.handleClosePress, delay + duration);
+    if (toast.duration > 0) {
+      this.hideTimer = setTimeout(this.handleClosePress, toast.duration);
     }
   }
 
diff --git a/superset/assets/src/dashboard/components/dnd/handleDrop.js b/superset/assets/src/dashboard/components/dnd/handleDrop.js
index 3739b18..faeeffa 100644
--- a/superset/assets/src/dashboard/components/dnd/handleDrop.js
+++ b/superset/assets/src/dashboard/components/dnd/handleDrop.js
@@ -1,4 +1,5 @@
 import getDropPosition, {
+  clearDropCache,
   DROP_TOP,
   DROP_RIGHT,
   DROP_BOTTOM,
@@ -75,6 +76,7 @@ export default function handleDrop(props, monitor, Component) {
   }
 
   onDrop(dropResult);
+  clearDropCache();
 
   return dropResult;
 }
diff --git a/superset/assets/src/dashboard/components/dnd/handleHover.js b/superset/assets/src/dashboard/components/dnd/handleHover.js
index a303e13..cb98a6f 100644
--- a/superset/assets/src/dashboard/components/dnd/handleHover.js
+++ b/superset/assets/src/dashboard/components/dnd/handleHover.js
@@ -1,7 +1,7 @@
 import throttle from 'lodash.throttle';
 import getDropPosition from '../../util/getDropPosition';
 
-const HOVER_THROTTLE_MS = 200;
+const HOVER_THROTTLE_MS = 150;
 
 function handleHover(props, monitor, Component) {
   // this may happen due to throttling
diff --git a/superset/assets/src/dashboard/components/gridComponents/ChartHolder.jsx b/superset/assets/src/dashboard/components/gridComponents/ChartHolder.jsx
index ab030f4..9ad9522 100644
--- a/superset/assets/src/dashboard/components/gridComponents/ChartHolder.jsx
+++ b/superset/assets/src/dashboard/components/gridComponents/ChartHolder.jsx
@@ -101,7 +101,7 @@ class ChartHolder extends React.Component {
       <DragDroppable
         component={component}
         parentComponent={parentComponent}
-        orientation={depth % 2 === 1 ? 'column' : 'row'}
+        orientation={parentComponent.type === ROW_TYPE ? 'column' : 'row'}
         index={index}
         depth={depth}
         onDrop={handleComponentDrop}
diff --git a/superset/assets/src/dashboard/components/gridComponents/Markdown.jsx b/superset/assets/src/dashboard/components/gridComponents/Markdown.jsx
index 459f89a..a49a893 100644
--- a/superset/assets/src/dashboard/components/gridComponents/Markdown.jsx
+++ b/superset/assets/src/dashboard/components/gridComponents/Markdown.jsx
@@ -41,8 +41,14 @@ const propTypes = {
 };
 
 const defaultProps = {};
-const markdownPlaceHolder = `### New Markdown
-Insert *bold* or _italic_ text, and (urls)[www.url.com] here.`;
+
+const markdownPlaceHolder = `# ✨Markdown
+## ✨Markdown
+### ✨Markdown
+
+<br />
+
+Click here to edit [markdown](https://bit.ly/1dQOfRK)`;
 
 class Markdown extends React.PureComponent {
   constructor(props) {
@@ -51,7 +57,7 @@ class Markdown extends React.PureComponent {
       isFocused: false,
       markdownSource: props.component.meta.code,
       editor: null,
-      editorMode: props.component.meta.code ? 'preview' : 'edit', // show edit mode when code is empty
+      editorMode: 'preview',
     };
 
     this.handleChangeFocus = this.handleChangeFocus.bind(this);
@@ -61,6 +67,13 @@ class Markdown extends React.PureComponent {
     this.setEditor = this.setEditor.bind(this);
   }
 
+  componentWillReceiveProps(nextProps) {
+    const nextSource = nextProps.component.meta.code;
+    if (this.state.markdownSource !== nextSource) {
+      this.setState({ markdownSource: nextSource });
+    }
+  }
+
   componentDidUpdate(prevProps) {
     if (
       this.state.editor &&
@@ -79,7 +92,10 @@ class Markdown extends React.PureComponent {
   }
 
   handleChangeFocus(nextFocus) {
-    this.setState(() => ({ isFocused: Boolean(nextFocus) }));
+    const nextFocused = !!nextFocus;
+    const nextEditMode = nextFocused ? 'edit' : 'preview';
+    this.setState(() => ({ isFocused: nextFocused }));
+    this.handleChangeEditorMode(nextEditMode);
   }
 
   handleChangeEditorMode(mode) {
@@ -120,10 +136,15 @@ class Markdown extends React.PureComponent {
         mode="markdown"
         theme="textmate"
         onChange={this.handleMarkdownChange}
-        width={'100%'}
-        height={'100%'}
+        width="100%"
+        height="100%"
         editorProps={{ $blockScrolling: true }}
-        value={this.state.markdownSource || markdownPlaceHolder}
+        value={
+          // thisl allows "select all => delete" to give an empty editor
+          typeof this.state.markdownSource === 'string'
+            ? this.state.markdownSource
+            : markdownPlaceHolder
+        }
         readOnly={false}
         onLoad={this.setEditor}
       />
@@ -132,7 +153,10 @@ class Markdown extends React.PureComponent {
 
   renderPreviewMode() {
     return (
-      <ReactMarkdown source={this.state.markdownSource} escapeHtml={false} />
+      <ReactMarkdown
+        source={this.state.markdownSource || markdownPlaceHolder}
+        escapeHtml={false}
+      />
     );
   }
 
@@ -163,7 +187,7 @@ class Markdown extends React.PureComponent {
       <DragDroppable
         component={component}
         parentComponent={parentComponent}
-        orientation={depth % 2 === 1 ? 'column' : 'row'}
+        orientation={parentComponent.type === ROW_TYPE ? 'column' : 'row'}
         index={index}
         depth={depth}
         onDrop={handleComponentDrop}
@@ -198,7 +222,9 @@ class Markdown extends React.PureComponent {
                 onResizeStart={onResizeStart}
                 onResize={onResize}
                 onResizeStop={onResizeStop}
-                editMode={editMode}
+                // disable resize when editing because if state is not synced
+                // with props it will reset the editor text to whatever props is
+                editMode={isFocused ? false : editMode}
               >
                 <div
                   ref={dragSourceRef}
@@ -208,10 +234,9 @@ class Markdown extends React.PureComponent {
                     ? this.renderEditMode()
                     : this.renderPreviewMode()}
                 </div>
-
-                {dropIndicatorProps && <div {...dropIndicatorProps} />}
               </ResizableContainer>
             </div>
+            {dropIndicatorProps && <div {...dropIndicatorProps} />}
           </WithPopoverMenu>
         )}
       </DragDroppable>
diff --git a/superset/assets/src/dashboard/components/gridComponents/Tab.jsx b/superset/assets/src/dashboard/components/gridComponents/Tab.jsx
index 63619c1..4cba2e6 100644
--- a/superset/assets/src/dashboard/components/gridComponents/Tab.jsx
+++ b/superset/assets/src/dashboard/components/gridComponents/Tab.jsx
@@ -92,6 +92,8 @@ export default class Tab extends React.PureComponent {
   renderTabContent() {
     const {
       component: tabComponent,
+      parentComponent: tabParentComponent,
+      index,
       depth,
       availableColumnCount,
       columnWidth,
@@ -117,6 +119,25 @@ export default class Tab extends React.PureComponent {
             onResizeStop={onResizeStop}
           />
         ))}
+        {/* Make the content of the tab component droppable in the case that there are no children */}
+        {tabComponent.children.length === 0 && (
+          <DragDroppable
+            component={tabComponent}
+            parentComponent={tabParentComponent}
+            orientation="column"
+            index={index}
+            depth={depth}
+            onDrop={this.handleDrop}
+            editMode
+            className="empty-tab-droptarget"
+          >
+            {({ dropIndicatorProps }) =>
+              dropIndicatorProps && (
+                <div className="drop-indicator drop-indicator--top" />
+              )
+            }
+          </DragDroppable>
+        )}
       </div>
     );
   }
@@ -136,7 +157,7 @@ export default class Tab extends React.PureComponent {
         // disable drag drop of top-level Tab's to prevent invalid nesting of a child in
         // itself, e.g. if a top-level Tab has a Tabs child, dragging the Tab into the Tabs would
         // reusult in circular children
-        disableDragDrop={depth === DASHBOARD_ROOT_DEPTH + 1}
+        disableDragDrop={depth <= DASHBOARD_ROOT_DEPTH + 1}
         editMode={editMode}
       >
         {({ dropIndicatorProps, dragSourceRef }) => (
diff --git a/superset/assets/src/dashboard/containers/Chart.jsx b/superset/assets/src/dashboard/containers/Chart.jsx
index 5631a25..c046c02 100644
--- a/superset/assets/src/dashboard/containers/Chart.jsx
+++ b/superset/assets/src/dashboard/containers/Chart.jsx
@@ -28,7 +28,7 @@ function mapStateToProps(
 
   return {
     chart,
-    datasource: chart && datasources[chart.form_data.datasource],
+    datasource: (chart && datasources[chart.form_data.datasource]) || {},
     slice: sliceEntities.slices[id],
     timeout: dashboardInfo.common.conf.SUPERSET_WEBSERVER_TIMEOUT,
     filters: filters[id] || EMPTY_FILTERS,
diff --git a/superset/assets/src/dashboard/containers/DashboardHeader.jsx b/superset/assets/src/dashboard/containers/DashboardHeader.jsx
index 19be06c..32eda1a 100644
--- a/superset/assets/src/dashboard/containers/DashboardHeader.jsx
+++ b/superset/assets/src/dashboard/containers/DashboardHeader.jsx
@@ -29,7 +29,7 @@ import { DASHBOARD_HEADER_ID } from '../util/constants';
 
 function mapStateToProps({
   dashboardLayout: undoableLayout,
-  dashboardState: dashboard,
+  dashboardState,
   dashboardInfo,
   charts,
 }) {
@@ -38,19 +38,20 @@ function mapStateToProps({
     undoLength: undoableLayout.past.length,
     redoLength: undoableLayout.future.length,
     layout: undoableLayout.present,
-    filters: dashboard.filters,
+    filters: dashboardState.filters,
     dashboardTitle: (
       (undoableLayout.present[DASHBOARD_HEADER_ID] || {}).meta || {}
     ).text,
-    expandedSlices: dashboard.expandedSlices,
-    css: dashboard.css,
+    expandedSlices: dashboardState.expandedSlices,
+    css: dashboardState.css,
     charts,
     userId: dashboardInfo.userId,
-    isStarred: !!dashboard.isStarred,
-    hasUnsavedChanges: !!dashboard.hasUnsavedChanges,
-    maxUndoHistoryExceeded: !!dashboard.maxUndoHistoryExceeded,
-    editMode: !!dashboard.editMode,
-    showBuilderPane: !!dashboard.showBuilderPane,
+    isStarred: !!dashboardState.isStarred,
+    hasUnsavedChanges: !!dashboardState.hasUnsavedChanges,
+    maxUndoHistoryExceeded: !!dashboardState.maxUndoHistoryExceeded,
+    editMode: !!dashboardState.editMode,
+    showBuilderPane: !!dashboardState.showBuilderPane,
+    isV2Preview: dashboardState.isV2Preview,
   };
 }
 
diff --git a/superset/assets/src/dashboard/deprecated/PromptV2ConversionModal.jsx b/superset/assets/src/dashboard/deprecated/PromptV2ConversionModal.jsx
new file mode 100644
index 0000000..876fa78
--- /dev/null
+++ b/superset/assets/src/dashboard/deprecated/PromptV2ConversionModal.jsx
@@ -0,0 +1,102 @@
+import moment from 'moment';
+import React from 'react';
+import PropTypes from 'prop-types';
+import { Modal, Button } from 'react-bootstrap';
+import { Logger, LOG_ACTIONS_READ_ABOUT_V2_CHANGES } from '../../logger';
+import { t } from '../../locales';
+
+const propTypes = {
+  v2FeedbackUrl: PropTypes.string,
+  v2AutoConvertDate: PropTypes.string,
+  onClose: PropTypes.func.isRequired,
+  handleConvertToV2: PropTypes.func.isRequired,
+  forceV2Edit: PropTypes.bool.isRequired,
+};
+
+const defaultProps = {
+  v2FeedbackUrl: null,
+  v2AutoConvertDate: null,
+};
+
+function logReadAboutV2Changes() {
+  Logger.append(LOG_ACTIONS_READ_ABOUT_V2_CHANGES, { version: 'v1' }, true);
+}
+
+function PromptV2ConversionModal({
+  v2FeedbackUrl,
+  v2AutoConvertDate,
+  onClose,
+  handleConvertToV2,
+  forceV2Edit,
+}) {
+  const timeUntilAutoConversion = v2AutoConvertDate
+    ? `approximately ${moment(v2AutoConvertDate).toNow(
+        true,
+      )} (${v2AutoConvertDate})` // eg 2 weeks (MM-DD-YYYY)
+    : 'a limited amount of time';
+
+  return (
+    <Modal onHide={onClose} onExit={onClose} animation show>
+      <Modal.Header closeButton>
+        <div style={{ fontSize: 20, fontWeight: 200, margin: '0px 4px -4px' }}>
+          {t('Convert to Dashboard v2 🎉')}
+        </div>
+      </Modal.Header>
+      <Modal.Body>
+        <h4>{t('Who')}</h4>
+        <p>
+          {t(
+            "As this dashboard's owner or a Superset Admin, we're soliciting your help to ensure a successful transition to the new dashboard experience.",
+          )}
+        </p>
+        <br />
+        <h4>{t('What and When')}</h4>
+        <p>
+          {t('You have ')}
+          <strong>
+            {timeUntilAutoConversion}
+            {t(' to convert this v1 dashboard to the new v2 format')}
+          </strong>
+          {t(' before it is auto-converted. ')}
+          {forceV2Edit && (
+            <em>
+              {t(
+                'Note that you may only edit dashboards using the v2 experience.',
+              )}
+            </em>
+          )}
+          {t('You may read more about these changes ')}
+          <a
+            target="_blank"
+            rel="noopener noreferrer"
+            href="https://gist.github.com/williaster/bad4ac9c6a71b234cf9fc8ee629844e5#file-superset-dashboard-v2-md"
+            onClick={logReadAboutV2Changes}
+          >
+            here
+          </a>
+          {v2FeedbackUrl ? t(' or ') : ''}
+          {v2FeedbackUrl ? (
+            <a target="_blank" rel="noopener noreferrer" href={v2FeedbackUrl}>
+              {t('provide feedback')}
+            </a>
+          ) : (
+            ''
+          )}.
+        </p>
+      </Modal.Body>
+      <Modal.Footer>
+        <Button onClick={onClose}>
+          {t(`${forceV2Edit ? 'View in' : 'Continue with'}  v1`)}
+        </Button>
+        <Button bsStyle="primary" onClick={handleConvertToV2}>
+          {t('Preview v2')}
+        </Button>
+      </Modal.Footer>
+    </Modal>
+  );
+}
+
+PromptV2ConversionModal.propTypes = propTypes;
+PromptV2ConversionModal.defaultProps = defaultProps;
+
+export default PromptV2ConversionModal;
diff --git a/superset/assets/src/dashboard/deprecated/V2PreviewModal.jsx b/superset/assets/src/dashboard/deprecated/V2PreviewModal.jsx
new file mode 100644
index 0000000..a0b7eed
--- /dev/null
+++ b/superset/assets/src/dashboard/deprecated/V2PreviewModal.jsx
@@ -0,0 +1,148 @@
+/* eslint-env browser */
+import moment from 'moment';
+import React from 'react';
+import PropTypes from 'prop-types';
+import { Modal, Button } from 'react-bootstrap';
+import { connect } from 'react-redux';
+import {
+  Logger,
+  LOG_ACTIONS_READ_ABOUT_V2_CHANGES,
+  LOG_ACTIONS_FALLBACK_TO_V1,
+} from '../../logger';
+
+import { t } from '../../locales';
+
+const propTypes = {
+  v2FeedbackUrl: PropTypes.string,
+  v2AutoConvertDate: PropTypes.string,
+  forceV2Edit: PropTypes.bool.isRequired,
+  onClose: PropTypes.func.isRequired,
+};
+
+const defaultProps = {
+  v2FeedbackUrl: null,
+  v2AutoConvertDate: null,
+  handleFallbackToV1: null,
+};
+
+// This is a gross component but it is temporary!
+class V2PreviewModal extends React.Component {
+  static logReadAboutV2Changes() {
+    Logger.append(
+      LOG_ACTIONS_READ_ABOUT_V2_CHANGES,
+      { version: 'v2-preview' },
+      true,
+    );
+  }
+
+  constructor(props) {
+    super(props);
+    this.handleFallbackToV1 = this.handleFallbackToV1.bind(this);
+  }
+
+  handleFallbackToV1() {
+    Logger.append(
+      LOG_ACTIONS_FALLBACK_TO_V1,
+      {
+        force_v2_edit: this.props.forceV2Edit,
+      },
+      true,
+    );
+    const url = new URL(window.location); // eslint-disable-line
+    url.searchParams.set('version', 'v1');
+    url.searchParams.delete('edit'); // remove JIC they were editing and v1 editing is not allowed
+    window.location = url;
+  }
+
+  render() {
+    const { v2FeedbackUrl, v2AutoConvertDate, onClose } = this.props;
+
+    const timeUntilAutoConversion = v2AutoConvertDate
+      ? `approximately ${moment(v2AutoConvertDate).toNow(
+          true,
+        )} (${v2AutoConvertDate})` // eg 2 weeks (MM-DD-YYYY)
+      : 'a limited amount of time';
+
+    return (
+      <Modal onHide={onClose} onExit={onClose} animation show>
+        <Modal.Header closeButton>
+          <div
+            style={{ fontSize: 20, fontWeight: 200, margin: '0px 4px -4px' }}
+          >
+            {t('Welcome to the new Dashboard v2 experience! 🎉')}
+          </div>
+        </Modal.Header>
+        <Modal.Body>
+          <h3>{t('Who')}</h3>
+          <p>
+            {t(
+              "As this dashboard's owner or a Superset Admin, we're soliciting your help to ensure a successful transition to the new dashboard experience. You can learn more about these changes ",
+            )}
+            <a
+              target="_blank"
+              rel="noopener noreferrer"
+              href="https://gist.github.com/williaster/bad4ac9c6a71b234cf9fc8ee629844e5#file-superset-dashboard-v2-md"
+              onClick={V2PreviewModal.logReadAboutV2Changes}
+            >
+              here
+            </a>
+            {v2FeedbackUrl ? t(' or ') : ''}
+            {v2FeedbackUrl ? (
+              <a target="_blank" rel="noopener noreferrer" href={v2FeedbackUrl}>
+                {t('provide feedback')}
+              </a>
+            ) : (
+              ''
+            )}.
+          </p>
+          <br />
+          <h3>{t('What')}</h3>
+          <p>
+            {t('You are ')}
+            <strong>{t('previewing')}</strong>
+            {t(
+              ' an auto-converted v2 version of your v1 dashboard. This conversion may have introduced regressions, such as minor layout variation or incompatible custom CSS. ',
+            )}
+            <strong>
+              {t(
+                'To persist your dashboard as v2, please make any necessary changes and save the dashboard',
+              )}
+            </strong>
+            {t(
+              '. Note that non-owners/-admins will continue to see the original version until you take this action.',
+            )}
+          </p>
+          <br />
+          <h3>{t('When')}</h3>
+          <p>
+            {t('You have ')}
+            <strong>
+              {timeUntilAutoConversion}
+              {t(' to edit and save this version ')}
+            </strong>
+            {t(
+              ' before it is auto-persisted to this preview. Upon save you will no longer be able to use the v1 experience.',
+            )}
+          </p>
+        </Modal.Body>
+        <Modal.Footer>
+          <Button onClick={this.handleFallbackToV1}>
+            {t('Fallback to v1')}
+          </Button>
+          <Button bsStyle="primary" onClick={onClose}>
+            {t('Preview v2')}
+          </Button>
+        </Modal.Footer>
+      </Modal>
+    );
+  }
+}
+
+V2PreviewModal.propTypes = propTypes;
+V2PreviewModal.defaultProps = defaultProps;
+
+export default connect(({ dashboardInfo }) => ({
+  v2FeedbackUrl: dashboardInfo.v2FeedbackUrl,
+  v2AutoConvertDate: dashboardInfo.v2AutoConvertDate,
+  forceV2Edit: dashboardInfo.forceV2Edit,
+}))(V2PreviewModal);
diff --git a/superset/assets/src/chart/Chart.jsx b/superset/assets/src/dashboard/deprecated/chart/Chart.jsx
similarity index 58%
copy from superset/assets/src/chart/Chart.jsx
copy to superset/assets/src/dashboard/deprecated/chart/Chart.jsx
index 060249f..bade493 100644
--- a/superset/assets/src/chart/Chart.jsx
+++ b/superset/assets/src/dashboard/deprecated/chart/Chart.jsx
@@ -4,20 +4,20 @@ import PropTypes from 'prop-types';
 import Mustache from 'mustache';
 import { Tooltip } from 'react-bootstrap';
 
-import { d3format } from '../modules/utils';
+import { d3format } from '../../../modules/utils';
 import ChartBody from './ChartBody';
-import Loading from '../components/Loading';
-import { Logger, LOG_ACTIONS_RENDER_CHART } from '../logger';
-import StackTraceMessage from '../components/StackTraceMessage';
-import RefreshChartOverlay from '../components/RefreshChartOverlay';
-import visMap from '../visualizations';
-import sandboxedEval from '../modules/sandbox';
+import Loading from '../../../components/Loading';
+import { Logger, LOG_ACTIONS_RENDER_CHART } from '../../../logger';
+import StackTraceMessage from '../../../components/StackTraceMessage';
+import RefreshChartOverlay from '../../../components/RefreshChartOverlay';
+import visMap from '../../../visualizations';
+import sandboxedEval from '../../../modules/sandbox';
 import './chart.css';
 
 const propTypes = {
   annotationData: PropTypes.object,
   actions: PropTypes.object,
-  chartId: PropTypes.number.isRequired,
+  chartKey: PropTypes.string.isRequired,
   containerId: PropTypes.string.isRequired,
   datasource: PropTypes.object.isRequired,
   formData: PropTypes.object.isRequired,
@@ -42,6 +42,8 @@ const propTypes = {
   // dashboard callbacks
   addFilter: PropTypes.func,
   getFilters: PropTypes.func,
+  clearFilter: PropTypes.func,
+  removeFilter: PropTypes.func,
   onQuery: PropTypes.func,
   onDismissRefreshOverlay: PropTypes.func,
 };
@@ -49,6 +51,8 @@ const propTypes = {
 const defaultProps = {
   addFilter: () => ({}),
   getFilters: () => ({}),
+  clearFilter: () => ({}),
+  removeFilter: () => ({}),
 };
 
 class Chart extends React.PureComponent {
@@ -63,6 +67,8 @@ 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);
     this.width = this.width.bind(this);
@@ -70,11 +76,10 @@ class Chart extends React.PureComponent {
 
   componentDidMount() {
     if (this.props.triggerQuery) {
-      const { formData } = this.props;
-      this.props.actions.runQuery(formData, false, this.props.timeout, this.props.chartId);
-    } else {
-      // when drag/dropping in a dashboard, a chart may be unmounted/remounted but still have data
-      this.renderViz();
+      this.props.actions.runQuery(this.props.formData, false,
+        this.props.timeout,
+        this.props.chartKey,
+      );
     }
   }
 
@@ -88,10 +93,10 @@ 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 ||
+        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 ||
@@ -113,14 +118,20 @@ 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);
+  }
+
   clearError() {
     this.setState({ errorMsg: null });
   }
 
   width() {
-    return (
-      this.props.width || (this.container && this.container.el && this.container.el.offsetWidth)
-    );
+    return this.props.width || this.container.el.offsetWidth;
   }
 
   headerHeight() {
@@ -128,9 +139,7 @@ class Chart extends React.PureComponent {
   }
 
   height() {
-    return (
-      this.props.height || (this.container && this.container.el && this.container.el.offsetHeight)
-    );
+    return this.props.height || this.container.el.offsetHeight;
   }
 
   d3format(col, number) {
@@ -141,7 +150,7 @@ class Chart extends React.PureComponent {
   }
 
   error(e) {
-    this.props.actions.chartRenderingFailed(e, this.props.chartId);
+    this.props.actions.chartRenderingFailed(e, this.props.chartKey);
   }
 
   verboseMetricName(metric) {
@@ -158,6 +167,7 @@ class Chart extends React.PureComponent {
 
   renderTooltip() {
     if (this.state.tooltip) {
+      /* eslint-disable react/no-danger */
       return (
         <Tooltip
           className="chart-tooltip"
@@ -167,83 +177,77 @@ class Chart extends React.PureComponent {
           positionLeft={this.state.tooltip.x + 30}
           arrowOffsetTop={10}
         >
-          <div // eslint-disable-next-line react/no-danger
-            dangerouslySetInnerHTML={{ __html: this.state.tooltip.content }}
-          />
+          <div dangerouslySetInnerHTML={{ __html: this.state.tooltip.content }} />
         </Tooltip>
       );
+      /* eslint-enable react/no-danger */
     }
     return null;
   }
 
   renderViz() {
-    const { vizType, formData, queryResponse, setControlValue, chartId, chartStatus } = this.props;
-    const visRenderer = visMap[vizType];
+    const viz = visMap[this.props.vizType];
+    const fd = this.props.formData;
+    const qr = this.props.queryResponse;
     const renderStart = Logger.getTimestamp();
     try {
       // Executing user-defined data mutator function
-      if (formData.js_data) {
-        queryResponse.data = sandboxedEval(formData.js_data)(queryResponse.data);
-      }
-      visRenderer(this, queryResponse, setControlValue);
-      if (chartStatus !== 'rendered') {
-        this.props.actions.chartRenderingSucceeded(chartId);
+      if (fd.js_data) {
+        qr.data = sandboxedEval(fd.js_data)(qr.data);
       }
+      // [re]rendering the visualization
+      viz(this, qr, this.props.setControlValue);
       Logger.append(LOG_ACTIONS_RENDER_CHART, {
-        label: 'slice_' + chartId,
-        vis_type: vizType,
+        slice_id: this.props.chartKey,
+        viz_type: this.props.vizType,
         start_offset: renderStart,
         duration: Logger.getTimestamp() - renderStart,
       });
-      this.props.actions.chartRenderingSucceeded(chartId);
+      this.props.actions.chartRenderingSucceeded(this.props.chartKey);
     } catch (e) {
-      console.error(e); // eslint-disable-line no-console
-      this.props.actions.chartRenderingFailed(e, chartId);
+      this.props.actions.chartRenderingFailed(e, this.props.chartKey);
     }
   }
 
   render() {
     const isLoading = this.props.chartStatus === 'loading';
-
-    // this allows <Loading /> to be positioned in the middle of the chart
-    const containerStyles = isLoading ? { height: this.height(), width: this.width() } : null;
     return (
-      <div className={`chart-container ${isLoading ? 'is-loading' : ''}`} style={containerStyles}>
+      <div className={`token col-md-12 ${isLoading ? 'is-loading' : ''}`}>
         {this.renderTooltip()}
-        {isLoading && <Loading size={75} />}
-        {this.props.chartAlert && (
-          <StackTraceMessage
-            message={this.props.chartAlert}
-            queryResponse={this.props.queryResponse}
-          />
-        )}
+        {isLoading &&
+          <Loading size={25} />
+        }
+        {this.props.chartAlert &&
+        <StackTraceMessage
+          message={this.props.chartAlert}
+          queryResponse={this.props.queryResponse}
+        />
+        }
 
         {!isLoading &&
           !this.props.chartAlert &&
           this.props.refreshOverlayVisible &&
           !this.props.errorMessage &&
-          this.container && (
-            <RefreshChartOverlay
-              height={this.height()}
-              width={this.width()}
-              onQuery={this.props.onQuery}
-              onDismiss={this.props.onDismissRefreshOverlay}
-            />
-          )}
-
-        {!isLoading &&
-          !this.props.chartAlert && (
-            <ChartBody
-              containerId={this.containerId}
-              vizType={this.props.vizType}
-              height={this.height}
-              width={this.width}
-              faded={this.props.refreshOverlayVisible && !this.props.errorMessage}
-              ref={(inner) => {
-                this.container = inner;
-              }}
-            />
-          )}
+          this.container &&
+          <RefreshChartOverlay
+            height={this.height()}
+            width={this.width()}
+            onQuery={this.props.onQuery}
+            onDismiss={this.props.onDismissRefreshOverlay}
+          />
+        }
+        {!isLoading && !this.props.chartAlert &&
+          <ChartBody
+            containerId={this.containerId}
+            vizType={this.props.vizType}
+            height={this.height}
+            width={this.width}
+            faded={this.props.refreshOverlayVisible && !this.props.errorMessage}
+            ref={(inner) => {
+              this.container = inner;
+            }}
+          />
+        }
       </div>
     );
   }
diff --git a/superset/assets/src/dashboard/deprecated/chart/ChartBody.jsx b/superset/assets/src/dashboard/deprecated/chart/ChartBody.jsx
new file mode 100644
index 0000000..b459f44
--- /dev/null
+++ b/superset/assets/src/dashboard/deprecated/chart/ChartBody.jsx
@@ -0,0 +1,55 @@
+import React from 'react';
+import PropTypes from 'prop-types';
+import $ from 'jquery';
+
+const propTypes = {
+  containerId: PropTypes.string.isRequired,
+  vizType: PropTypes.string.isRequired,
+  height: PropTypes.func.isRequired,
+  width: PropTypes.func.isRequired,
+  faded: PropTypes.bool,
+};
+
+class ChartBody extends React.PureComponent {
+  html(data) {
+    this.el.innerHTML = data;
+  }
+
+  css(property, value) {
+    this.el.style[property] = value;
+  }
+
+  get(n) {
+    return $(this.el).get(n);
+  }
+
+  find(classname) {
+    return $(this.el).find(classname);
+  }
+
+  show() {
+    return $(this.el).show();
+  }
+
+  height() {
+    return this.props.height();
+  }
+
+  width() {
+    return this.props.width();
+  }
+
+  render() {
+    return (
+      <div
+        id={this.props.containerId}
+        className={`slice_container ${this.props.vizType}${this.props.faded ? ' faded' : ''}`}
+        ref={(el) => { this.el = el; }}
+      />
+    );
+  }
+}
+
+ChartBody.propTypes = propTypes;
+
+export default ChartBody;
diff --git a/superset/assets/src/dashboard/deprecated/chart/ChartContainer.jsx b/superset/assets/src/dashboard/deprecated/chart/ChartContainer.jsx
new file mode 100644
index 0000000..b731412
--- /dev/null
+++ b/superset/assets/src/dashboard/deprecated/chart/ChartContainer.jsx
@@ -0,0 +1,29 @@
+import { connect } from 'react-redux';
+import { bindActionCreators } from 'redux';
+
+import * as Actions from './chartAction';
+import Chart from './Chart';
+
+function mapStateToProps({ charts }, ownProps) {
+  const chart = charts[ownProps.chartKey];
+  return {
+    annotationData: chart.annotationData,
+    chartAlert: chart.chartAlert,
+    chartStatus: chart.chartStatus,
+    chartUpdateEndTime: chart.chartUpdateEndTime,
+    chartUpdateStartTime: chart.chartUpdateStartTime,
+    latestQueryFormData: chart.latestQueryFormData,
+    lastRendered: chart.lastRendered,
+    queryResponse: chart.queryResponse,
+    queryRequest: chart.queryRequest,
+    triggerQuery: chart.triggerQuery,
+  };
+}
+
+function mapDispatchToProps(dispatch) {
+  return {
+    actions: bindActionCreators(Actions, dispatch),
+  };
+}
+
+export default connect(mapStateToProps, mapDispatchToProps)(Chart);
diff --git a/superset/assets/src/dashboard/deprecated/chart/chart.css b/superset/assets/src/dashboard/deprecated/chart/chart.css
new file mode 100644
index 0000000..eda2054
--- /dev/null
+++ b/superset/assets/src/dashboard/deprecated/chart/chart.css
@@ -0,0 +1,4 @@
+.chart-tooltip {
+  opacity: 0.75;
+  font-size: 12px;
+}
diff --git a/superset/assets/src/dashboard/deprecated/chart/chartAction.js b/superset/assets/src/dashboard/deprecated/chart/chartAction.js
new file mode 100644
index 0000000..52f9c47
--- /dev/null
+++ b/superset/assets/src/dashboard/deprecated/chart/chartAction.js
@@ -0,0 +1,195 @@
+import { getExploreUrlAndPayload, getAnnotationJsonUrl } from '../../../explore/exploreUtils';
+import { requiresQuery, ANNOTATION_SOURCE_TYPES } from '../../../modules/AnnotationTypes';
+import { Logger, LOG_ACTIONS_LOAD_CHART } from '../../../logger';
+import { COMMON_ERR_MESSAGES } from '../../../common';
+import { t } from '../../../locales';
+
+const $ = window.$ = require('jquery');
+
+export const CHART_UPDATE_STARTED = 'CHART_UPDATE_STARTED';
+export function chartUpdateStarted(queryRequest, latestQueryFormData, key) {
+  return { type: CHART_UPDATE_STARTED, queryRequest, latestQueryFormData, key };
+}
+
+export const CHART_UPDATE_SUCCEEDED = 'CHART_UPDATE_SUCCEEDED';
+export function chartUpdateSucceeded(queryResponse, key) {
+  return { type: CHART_UPDATE_SUCCEEDED, queryResponse, key };
+}
+
+export const CHART_UPDATE_STOPPED = 'CHART_UPDATE_STOPPED';
+export function chartUpdateStopped(key) {
+  return { type: CHART_UPDATE_STOPPED, key };
+}
+
+export const CHART_UPDATE_TIMEOUT = 'CHART_UPDATE_TIMEOUT';
+export function chartUpdateTimeout(statusText, timeout, key) {
+  return { type: CHART_UPDATE_TIMEOUT, statusText, timeout, key };
+}
+
+export const CHART_UPDATE_FAILED = 'CHART_UPDATE_FAILED';
+export function chartUpdateFailed(queryResponse, key) {
+  return { type: CHART_UPDATE_FAILED, queryResponse, key };
+}
+
+export const CHART_RENDERING_FAILED = 'CHART_RENDERING_FAILED';
+export function chartRenderingFailed(error, key) {
+  return { type: CHART_RENDERING_FAILED, error, key };
+}
+
+export const CHART_RENDERING_SUCCEEDED = 'CHART_RENDERING_SUCCEEDED';
+export function chartRenderingSucceeded(key) {
+  return { type: CHART_RENDERING_SUCCEEDED, key };
+}
+
+export const REMOVE_CHART = 'REMOVE_CHART';
+export function removeChart(key) {
+  return { type: REMOVE_CHART, key };
+}
+
+export const ANNOTATION_QUERY_SUCCESS = 'ANNOTATION_QUERY_SUCCESS';
+export function annotationQuerySuccess(annotation, queryResponse, key) {
+  return { type: ANNOTATION_QUERY_SUCCESS, annotation, queryResponse, key };
+}
+
+export const ANNOTATION_QUERY_STARTED = 'ANNOTATION_QUERY_STARTED';
+export function annotationQueryStarted(annotation, queryRequest, key) {
+  return { type: ANNOTATION_QUERY_STARTED, annotation, queryRequest, key };
+}
+
+export const ANNOTATION_QUERY_FAILED = 'ANNOTATION_QUERY_FAILED';
+export function annotationQueryFailed(annotation, queryResponse, key) {
+  return { type: ANNOTATION_QUERY_FAILED, annotation, queryResponse, key };
+}
+
+export function runAnnotationQuery(annotation, timeout = 60, formData = null, key) {
+  return function (dispatch, getState) {
+    const sliceKey = key || Object.keys(getState().charts)[0];
+    const fd = formData || getState().charts[sliceKey].latestQueryFormData;
+
+    if (!requiresQuery(annotation.sourceType)) {
+      return Promise.resolve();
+    }
+
+    const granularity = fd.time_grain_sqla || fd.granularity;
+    fd.time_grain_sqla = granularity;
+    fd.granularity = granularity;
+
+    const sliceFormData = Object.keys(annotation.overrides)
+      .reduce((d, k) => ({
+        ...d,
+        [k]: annotation.overrides[k] || fd[k],
+      }), {});
+    const isNative = annotation.sourceType === ANNOTATION_SOURCE_TYPES.NATIVE;
+    const url = getAnnotationJsonUrl(annotation.value, sliceFormData, isNative);
+    const queryRequest = $.ajax({
+      url,
+      dataType: 'json',
+      timeout: timeout * 1000,
+    });
+    dispatch(annotationQueryStarted(annotation, queryRequest, sliceKey));
+    return queryRequest
+      .then(queryResponse => dispatch(annotationQuerySuccess(annotation, queryResponse, sliceKey)))
+      .catch((err) => {
+        if (err.statusText === 'timeout') {
+          dispatch(annotationQueryFailed(annotation, { error: 'Query Timeout' }, sliceKey));
+        } else if ((err.responseJSON.error || '').toLowerCase().startsWith('no data')) {
+          dispatch(annotationQuerySuccess(annotation, err, sliceKey));
+        } else if (err.statusText !== 'abort') {
+          dispatch(annotationQueryFailed(annotation, err.responseJSON, sliceKey));
+        }
+      });
+  };
+}
+
+export const TRIGGER_QUERY = 'TRIGGER_QUERY';
+export function triggerQuery(value = true, key) {
+  return { type: TRIGGER_QUERY, value, key };
+}
+
+// this action is used for forced re-render without fetch data
+export const RENDER_TRIGGERED = 'RENDER_TRIGGERED';
+export function renderTriggered(value, key) {
+  return { type: RENDER_TRIGGERED, value, key };
+}
+
+export const UPDATE_QUERY_FORM_DATA = 'UPDATE_QUERY_FORM_DATA';
+export function updateQueryFormData(value, key) {
+  return { type: UPDATE_QUERY_FORM_DATA, value, key };
+}
+
+export const RUN_QUERY = 'RUN_QUERY';
+export function runQuery(formData, force = false, timeout = 60, key) {
+  return (dispatch) => {
+    const { url, payload } = getExploreUrlAndPayload({
+      formData,
+      endpointType: 'json',
+      force,
+    });
+    const logStart = Logger.getTimestamp();
+    const queryRequest = $.ajax({
+      type: 'POST',
+      url,
+      dataType: 'json',
+      data: {
+        form_data: JSON.stringify(payload),
+      },
+      timeout: timeout * 1000,
+    });
+    const queryPromise = Promise.resolve(dispatch(chartUpdateStarted(queryRequest, payload, key)))
+      .then(() => queryRequest)
+      .then((queryResponse) => {
+        Logger.append(LOG_ACTIONS_LOAD_CHART, {
+          slice_id: 'slice_' + key,
+          is_cached: queryResponse.is_cached,
+          force_refresh: force,
+          row_count: queryResponse.rowcount,
+          datasource: formData.datasource,
+          start_offset: logStart,
+          duration: Logger.getTimestamp() - logStart,
+          has_extra_filters: formData.extra_filters && formData.extra_filters.length > 0,
+          viz_type: formData.viz_type,
+        });
+        return dispatch(chartUpdateSucceeded(queryResponse, key));
+      })
+      .catch((err) => {
+        Logger.append(LOG_ACTIONS_LOAD_CHART, {
+          slice_id: 'slice_' + key,
+          has_err: true,
+          datasource: formData.datasource,
+          start_offset: logStart,
+          duration: Logger.getTimestamp() - logStart,
+        });
+        if (err.statusText === 'timeout') {
+          dispatch(chartUpdateTimeout(err.statusText, timeout, key));
+        } else if (err.statusText === 'abort') {
+          dispatch(chartUpdateStopped(key));
+        } else {
+          let errObject;
+          if (err.responseJSON) {
+            errObject = err.responseJSON;
+          } else if (err.stack) {
+            errObject = {
+              error: t('Unexpected error: ') + err.description,
+              stacktrace: err.stack,
+            };
+          } else if (err.responseText && err.responseText.indexOf('CSRF') >= 0) {
+            errObject = {
+              error: COMMON_ERR_MESSAGES.SESSION_TIMED_OUT,
+            };
+          } else {
+            errObject = {
+              error: t('Unexpected error.'),
+            };
+          }
+          dispatch(chartUpdateFailed(errObject, key));
+        }
+      });
+    const annotationLayers = formData.annotation_layers || [];
+    return Promise.all([
+      queryPromise,
+      dispatch(triggerQuery(false, key)),
+      dispatch(updateQueryFormData(payload, key)),
+      ...annotationLayers.map(x => dispatch(runAnnotationQuery(x, timeout, formData, key))),
+    ]);
+  };
+}
diff --git a/superset/assets/src/dashboard/deprecated/chart/chartReducer.js b/superset/assets/src/dashboard/deprecated/chart/chartReducer.js
new file mode 100644
index 0000000..8d11249
--- /dev/null
+++ b/superset/assets/src/dashboard/deprecated/chart/chartReducer.js
@@ -0,0 +1,158 @@
+/* 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: '',
+  chartAlert: null,
+  chartStatus: 'loading',
+  chartUpdateEndTime: null,
+  chartUpdateStartTime: now(),
+  latestQueryFormData: {},
+  queryRequest: null,
+  queryResponse: null,
+  triggerQuery: true,
+  lastRendered: 0,
+};
+
+export default function chartReducer(charts = {}, action) {
+  const actionHandlers = {
+    [actions.CHART_UPDATE_SUCCEEDED](state) {
+      return { ...state,
+        chartStatus: 'success',
+        queryResponse: action.queryResponse,
+        chartUpdateEndTime: now(),
+      };
+    },
+    [actions.CHART_UPDATE_STARTED](state) {
+      return { ...state,
+        chartStatus: 'loading',
+        chartAlert: null,
+        chartUpdateEndTime: null,
+        chartUpdateStartTime: now(),
+        queryRequest: action.queryRequest,
+      };
+    },
+    [actions.CHART_UPDATE_STOPPED](state) {
+      return { ...state,
+        chartStatus: 'stopped',
+        chartAlert: t('Updating chart was stopped'),
+      };
+    },
+    [actions.CHART_RENDERING_SUCCEEDED](state) {
+      return { ...state,
+        chartStatus: 'rendered',
+      };
+    },
+    [actions.CHART_RENDERING_FAILED](state) {
+      return { ...state,
+        chartStatus: 'failed',
+        chartAlert: t('An error occurred while rendering the visualization: %s', action.error),
+      };
+    },
+    [actions.CHART_UPDATE_TIMEOUT](state) {
+      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.')),
+      };
+    },
+    [actions.CHART_UPDATE_FAILED](state) {
+      return { ...state,
+        chartStatus: 'failed',
+        chartAlert: action.queryResponse ? action.queryResponse.error : t('Network error.'),
+        chartUpdateEndTime: now(),
+        queryResponse: action.queryResponse,
+      };
+    },
+    [actions.TRIGGER_QUERY](state) {
+      return { ...state, triggerQuery: action.value };
+    },
+    [actions.RENDER_TRIGGERED](state) {
+      return { ...state, lastRendered: action.value };
+    },
+    [actions.UPDATE_QUERY_FORM_DATA](state) {
+      return { ...state, latestQueryFormData: action.value };
+    },
+    [actions.ANNOTATION_QUERY_STARTED](state) {
+      if (state.annotationQuery &&
+        state.annotationQuery[action.annotation.name]) {
+        state.annotationQuery[action.annotation.name].abort();
+      }
+      const annotationQuery = {
+        ...state.annotationQuery,
+        [action.annotation.name]: action.queryRequest,
+      };
+      return {
+        ...state,
+        annotationQuery,
+      };
+    },
+    [actions.ANNOTATION_QUERY_SUCCESS](state) {
+      const annotationData = {
+        ...state.annotationData,
+        [action.annotation.name]: action.queryResponse.data,
+      };
+      const annotationError = { ...state.annotationError };
+      delete annotationError[action.annotation.name];
+      const annotationQuery = { ...state.annotationQuery };
+      delete annotationQuery[action.annotation.name];
+      return {
+        ...state,
+        annotationData,
+        annotationError,
+        annotationQuery,
+      };
+    },
+    [actions.ANNOTATION_QUERY_FAILED](state) {
+      const annotationData = { ...state.annotationData };
+      delete annotationData[action.annotation.name];
+      const annotationError = {
+        ...state.annotationError,
+        [action.annotation.name]: action.queryResponse ?
+          action.queryResponse.error : t('Network error.'),
+      };
+      const annotationQuery = { ...state.annotationQuery };
+      delete annotationQuery[action.annotation.name];
+      return {
+        ...state,
+        annotationData,
+        annotationError,
+        annotationQuery,
+      };
+    },
+  };
+
+  /* eslint-disable no-param-reassign */
+  if (action.type === actions.REMOVE_CHART) {
+    delete charts[action.key];
+    return charts;
+  }
+
+  if (action.type in actionHandlers) {
+    return { ...charts, [action.key]: actionHandlers[action.type](charts[action.key], action) };
+  }
+
+  return charts;
+}
diff --git a/superset/assets/src/dashboard/deprecated/v1/actions.js b/superset/assets/src/dashboard/deprecated/v1/actions.js
new file mode 100644
index 0000000..7381486
--- /dev/null
+++ b/superset/assets/src/dashboard/deprecated/v1/actions.js
@@ -0,0 +1,127 @@
+/* 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/deprecated/v1/components/CodeModal.jsx b/superset/assets/src/dashboard/deprecated/v1/components/CodeModal.jsx
new file mode 100644
index 0000000..3f802c3
--- /dev/null
+++ b/superset/assets/src/dashboard/deprecated/v1/components/CodeModal.jsx
@@ -0,0 +1,48 @@
+import React from 'react';
+import PropTypes from 'prop-types';
+
+import ModalTrigger from '../../../../components/ModalTrigger';
+import { t } from '../../../../locales';
+
+const propTypes = {
+  triggerNode: PropTypes.node.isRequired,
+  code: PropTypes.string,
+  codeCallback: PropTypes.func,
+};
+
+const defaultProps = {
+  codeCallback: () => {},
+};
+
+export default class CodeModal extends React.PureComponent {
+  constructor(props) {
+    super(props);
+    this.state = { code: props.code };
+  }
+  beforeOpen() {
+    let code = this.props.code;
+    if (!code && this.props.codeCallback) {
+      code = this.props.codeCallback();
+    }
+    this.setState({ code });
+  }
+  render() {
+    return (
+      <ModalTrigger
+        triggerNode={this.props.triggerNode}
+        isButton
+        beforeOpen={this.beforeOpen.bind(this)}
+        modalTitle={t('Active Dashboard Filters')}
+        modalBody={
+          <div className="CodeModal">
+            <pre>
+              {this.state.code}
+            </pre>
+          </div>
+        }
+      />
+    );
+  }
+}
+CodeModal.propTypes = propTypes;
+CodeModal.defaultProps = defaultProps;
diff --git a/superset/assets/src/dashboard/deprecated/v1/components/Controls.jsx b/superset/assets/src/dashboard/deprecated/v1/components/Controls.jsx
new file mode 100644
index 0000000..6a6fa47
--- /dev/null
+++ b/superset/assets/src/dashboard/deprecated/v1/components/Controls.jsx
@@ -0,0 +1,214 @@
+import React from 'react';
+import PropTypes from 'prop-types';
+import { DropdownButton, MenuItem } from 'react-bootstrap';
+
+import CssEditor from './CssEditor';
+import RefreshIntervalModal from './RefreshIntervalModal';
+import SaveModal from './SaveModal';
+import SliceAdder from './SliceAdder';
+import { t } from '../../../../locales';
+import InfoTooltipWithTrigger from '../../../../components/InfoTooltipWithTrigger';
+
+const $ = window.$ = require('jquery');
+
+const propTypes = {
+  dashboard: PropTypes.object.isRequired,
+  filters: 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,
+  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 || '',
+      cssTemplates: [],
+    };
+    this.refresh = this.refresh.bind(this);
+    this.toggleModal = this.toggleModal.bind(this);
+    this.updateDom = this.updateDom.bind(this);
+  }
+  componentWillMount() {
+    this.updateDom(this.state.css);
+
+    $.get('/csstemplateasyncmodelview/api/read', (data) => {
+      const cssTemplates = data.result.map(row => ({
+        value: row.template_name,
+        css: row.css,
+        label: row.template_name,
+      }));
+      this.setState({ cssTemplates });
+    });
+  }
+  refresh() {
+    // Force refresh all slices
+    this.props.renderSlices(true);
+  }
+  toggleModal(modal) {
+    let currentModal;
+    if (modal !== this.state.currentModal) {
+      currentModal = modal;
+    }
+    this.setState({ currentModal });
+  }
+  changeCss(css) {
+    this.setState({ css }, () => {
+      this.updateDom(css);
+    });
+    this.props.onChange();
+  }
+  updateDom(css) {
+    const className = 'CssEditor-css';
+    const head = document.head || document.getElementsByTagName('head')[0];
+    let style = document.querySelector('.' + className);
+
+    if (!style) {
+      style = document.createElement('style');
+      style.className = className;
+      style.type = 'text/css';
+      head.appendChild(style);
+    }
+    if (style.styleSheet) {
+      style.styleSheet.cssText = css;
+    } else {
+      style.innerHTML = css;
+    }
+  }
+  render() {
+    const { dashboard, userId, filters,
+      addSlicesToDashboard, startPeriodicRender,
+      serialize, 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}`;
+    let saveText = t('Save as');
+    if (editMode) {
+      saveText = t('Save');
+    }
+    return (
+      <span>
+        <DropdownButton title="Actions" bsSize="small" id="bg-nested-dropdown" pullRight>
+          <ActionMenuItem
+            text={t('Force Refresh')}
+            tooltip={t('Force refresh the whole dashboard')}
+            faIcon="refresh"
+            onClick={this.refresh}
+          />
+          <RefreshIntervalModal
+            onChange={refreshInterval => startPeriodicRender(refreshInterval * 1000)}
+            triggerNode={
+              <MenuItemContent
+                text={t('Set autorefresh')}
+                tooltip={t('Set the auto-refresh interval for this session')}
+                faIcon="clock-o"
+              />
+            }
+          />
+          {dashboard.dash_save_perm &&
+            <SaveModal
+              dashboard={dashboard}
+              filters={filters}
+              serialize={serialize}
+              onSave={onSave}
+              css={this.state.css}
+              triggerNode={
+                <MenuItemContent
+                  text={saveText}
+                  tooltip={t('Save the dashboard')}
+                  faIcon="save"
+                />
+              }
+            />
+          }
+          {editMode &&
+            <ActionMenuItem
+              text={t('Edit properties')}
+              tooltip={t("Edit the dashboards's properties")}
+              faIcon="edit"
+              onClick={() => { window.location = `/dashboardmodelview/edit/${dashboard.id}`; }}
+            />
+          }
+          {editMode &&
+            <ActionMenuItem
+              text={t('Email')}
+              tooltip={t('Email a link to this dashboard')}
+              onClick={() => { window.location = emailLink; }}
+              faIcon="envelope"
+            />
+          }
+          {editMode &&
+            <SliceAdder
+              dashboard={dashboard}
+              addSlicesToDashboard={addSlicesToDashboard}
+              userId={userId}
+              triggerNode={
+                <MenuItemContent
+                  text={t('Add Charts')}
+                  tooltip={t('Add some charts to this dashboard')}
+                  faIcon="plus"
+                />
+              }
+            />
+          }
+          {editMode &&
+            <CssEditor
+              dashboard={dashboard}
+              triggerNode={
+                <MenuItemContent
+                  text={t('Edit CSS')}
+                  tooltip={t('Change the style of the dashboard using CSS code')}
+                  faIcon="css3"
+                />
+              }
+              initialCss={this.state.css}
+              templates={this.state.cssTemplates}
+              onChange={this.changeCss.bind(this)}
+            />
+          }
+        </DropdownButton>
+      </span>
+    );
+  }
+}
+Controls.propTypes = propTypes;
+
+export default Controls;
diff --git a/superset/assets/src/dashboard/deprecated/v1/components/CssEditor.jsx b/superset/assets/src/dashboard/deprecated/v1/components/CssEditor.jsx
new file mode 100644
index 0000000..ee11ff2
--- /dev/null
+++ b/superset/assets/src/dashboard/deprecated/v1/components/CssEditor.jsx
@@ -0,0 +1,91 @@
+import React from 'react';
+import PropTypes from 'prop-types';
+import Select from 'react-select';
+
+import AceEditor from 'react-ace';
+import 'brace/mode/css';
+import 'brace/theme/github';
+
+import ModalTrigger from '../../../../components/ModalTrigger';
+import { t } from '../../../../locales';
+
+const propTypes = {
+  initialCss: PropTypes.string,
+  triggerNode: PropTypes.node.isRequired,
+  onChange: PropTypes.func,
+  templates: PropTypes.array,
+};
+
+const defaultProps = {
+  initialCss: '',
+  onChange: () => {},
+  templates: [],
+};
+
+class CssEditor extends React.PureComponent {
+  constructor(props) {
+    super(props);
+    this.state = {
+      css: props.initialCss,
+      cssTemplateOptions: [],
+    };
+  }
+  changeCss(css) {
+    this.setState({ css }, () => {
+      this.props.onChange(css);
+    });
+  }
+  changeCssTemplate(opt) {
+    this.changeCss(opt.css);
+  }
+  renderTemplateSelector() {
+    if (this.props.templates) {
+      return (
+        <div style={{ zIndex: 10 }}>
+          <h5>{t('Load a template')}</h5>
+          <Select
+            options={this.props.templates}
+            placeholder={t('Load a CSS template')}
+            onChange={this.changeCssTemplate.bind(this)}
+          />
+        </div>
+      );
+    }
+    return null;
+  }
+  render() {
+    return (
+      <ModalTrigger
+        triggerNode={this.props.triggerNode}
+        modalTitle={t('CSS')}
+        isMenuItem
+        modalBody={
+          <div>
+            {this.renderTemplateSelector()}
+            <div style={{ zIndex: 1 }}>
+              <h5>{t('Live CSS Editor')}</h5>
+              <div style={{ border: 'solid 1px grey' }}>
+                <AceEditor
+                  mode="css"
+                  theme="github"
+                  minLines={8}
+                  maxLines={30}
+                  onChange={this.changeCss.bind(this)}
+                  height="200px"
+                  width="100%"
+                  editorProps={{ $blockScrolling: true }}
+                  enableLiveAutocompletion
+                  value={this.state.css || ''}
+                />
+              </div>
+            </div>
+          </div>
+        }
+      />
+    );
+  }
+}
+CssEditor.propTypes = propTypes;
+CssEditor.defaultProps = defaultProps;
+
+export default CssEditor;
diff --git a/superset/assets/src/dashboard/deprecated/v1/components/Dashboard.jsx b/superset/assets/src/dashboard/deprecated/v1/components/Dashboard.jsx
new file mode 100644
index 0000000..6ba4159
--- /dev/null
+++ b/superset/assets/src/dashboard/deprecated/v1/components/Dashboard.jsx
@@ -0,0 +1,423 @@
+import React from 'react';
+import PropTypes from 'prop-types';
+
+import AlertsWrapper from '../../../../components/AlertsWrapper';
+import GridLayout from './GridLayout';
+import Header from './Header';
+import { exportChart } from '../../../../explore/exploreUtils';
+import { areObjectsEqual } from '../../../../reduxUtils';
+import {
+  Logger,
+  ActionLog,
+  DASHBOARD_EVENT_NAMES,
+  LOG_ACTIONS_MOUNT_DASHBOARD,
+  LOG_ACTIONS_EXPLORE_DASHBOARD_CHART,
+  LOG_ACTIONS_EXPORT_CSV_DASHBOARD_CHART,
+  LOG_ACTIONS_FIRST_DASHBOARD_LOAD,
+  LOG_ACTIONS_REFRESH_CHART,
+  LOG_ACTIONS_REFRESH_DASHBOARD,
+} from '../../../../logger';
+
+import { t } from '../../../../locales';
+
+import '../../../../../stylesheets/dashboard_deprecated.css';
+
+const propTypes = {
+  actions: PropTypes.object,
+  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,
+      source: 'dashboard',
+      sourceId: props.dashboard.id,
+      eventNames: DASHBOARD_EVENT_NAMES,
+    });
+    Logger.start(this.loadingLog);
+
+    // alert for unsaved changes
+    this.state = {
+      unsavedChanges: false,
+    };
+    this.handleSetEditMode = this.handleSetEditMode.bind(this);
+
+    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.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.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);
+  }
+
+  componentDidMount() {
+    window.addEventListener('resize', this.rerenderCharts);
+    this.ts_mount = new Date().getTime();
+    Logger.append(LOG_ACTIONS_MOUNT_DASHBOARD, { version: 'v1' });
+  }
+
+  componentWillReceiveProps(nextProps) {
+    if (this.firstLoad &&
+      Object.values(nextProps.slices)
+        .every(slice => (['rendered', 'failed', 'stopped'].indexOf(slice.chartStatus) > -1))
+    ) {
+      Logger.append(LOG_ACTIONS_FIRST_DASHBOARD_LOAD, {
+        duration: new Date().getTime() - this.ts_mount,
+        version: 'v1',
+      });
+      Logger.send(this.loadingLog);
+      this.firstLoad = false;
+    }
+  }
+
+  componentDidUpdate(prevProps) {
+    if (this.props.refresh) {
+      let changedFilterKey;
+      const prevFiltersKeySet = new Set(Object.keys(prevProps.filters));
+      Object.keys(this.props.filters).some((key) => {
+        prevFiltersKeySet.delete(key);
+        if (prevProps.filters[key] === undefined ||
+          !areObjectsEqual(prevProps.filters[key], this.props.filters[key])) {
+          changedFilterKey = key;
+          return true;
+        }
+        return false;
+      });
+      // has changed filter or removed a filter?
+      if (!!changedFilterKey || prevFiltersKeySet.size) {
+        this.refreshExcept(changedFilterKey);
+      }
+    }
+  }
+
+  componentWillUnmount() {
+    window.removeEventListener('resize', this.rerenderCharts);
+  }
+
+  onBeforeUnload(hasChanged) {
+    if (hasChanged) {
+      window.addEventListener('beforeunload', this.unload);
+    } else {
+      window.removeEventListener('beforeunload', this.unload);
+    }
+  }
+
+  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);
+  }
+
+  getFormDataExtra(slice) {
+    const formDataExtra = Object.assign({}, slice.formData);
+    formDataExtra.extra_filters = this.effectiveExtraFilters(slice.slice_id);
+    return formDataExtra;
+  }
+
+  getFilters(sliceId) {
+    return this.props.filters[sliceId];
+  }
+
+  unload() {
+    const message = t('You have unsaved changes.');
+    window.event.returnValue = message; // Gecko + IE
+    return message; // Gecko + Webkit, Safari, Chrome etc.
+  }
+
+  effectiveExtraFilters(sliceId) {
+    const metadata = this.props.dashboard.metadata;
+    const filters = this.props.filters;
+    const f = [];
+    const immuneSlices = metadata.filter_immune_slices || [];
+    if (sliceId && immuneSlices.includes(sliceId)) {
+      // The slice is immune to dashboard filters
+      return f;
+    }
+
+    // Building a list of fields the slice is immune to filters on
+    let immuneToFields = [];
+    if (
+      sliceId &&
+      metadata.filter_immune_slice_fields &&
+      metadata.filter_immune_slice_fields[sliceId]) {
+      immuneToFields = metadata.filter_immune_slice_fields[sliceId];
+    }
+    for (const filteringSliceId in filters) {
+      if (filteringSliceId === sliceId.toString()) {
+        // Filters applied by the slice don't apply to itself
+        continue;
+      }
+      for (const field in filters[filteringSliceId]) {
+        if (!immuneToFields.includes(field)) {
+          f.push({
+            col: field,
+            op: 'in',
+            val: filters[filteringSliceId][field],
+          });
+        }
+      }
+    }
+    return f;
+  }
+
+  refreshExcept(filterKey) {
+    const immune = this.props.dashboard.metadata.filter_immune_slices || [];
+    let slices = this.getAllSlices();
+    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, fetchingAllSlices = false) {
+    if (force && !fetchingAllSlices) {
+      const chartQuery = (this.props.slices[slice.chartKey] || {}).queryResponse;
+      Logger.append(
+        LOG_ACTIONS_REFRESH_CHART,
+        {
+          slice_id: slice.slice_id,
+          is_cached: chartQuery.is_cached,
+          version: 'v1',
+        },
+        true,
+      );
+    }
+    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();
+    Logger.append(
+      LOG_ACTIONS_REFRESH_DASHBOARD,
+      {
+        force,
+        interval,
+        chartCount: slices.length,
+        version: 'v1',
+      },
+      true,
+    );
+    if (!interval) {
+      slices.forEach((slice) => { this.fetchSlice(slice, force, true); });
+      return;
+    }
+
+    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, true); }, delay * i);
+    });
+  }
+
+  exploreChart(slice) {
+    const chartQuery = (this.props.slices[slice.chartKey] || {}).queryResponse;
+    Logger.append(
+      LOG_ACTIONS_EXPLORE_DASHBOARD_CHART,
+      {
+        slice_id: slice.slice_id,
+        is_cached: chartQuery && chartQuery.is_cached,
+        version: 'v1',
+      },
+      true,
+    );
+    const formData = this.getFormDataExtra(slice);
+    exportChart(formData);
+  }
+
+  exportCSV(slice) {
+    const chartQuery = (this.props.slices[slice.chartKey] || {}).queryResponse;
+    Logger.append(
+      LOG_ACTIONS_EXPORT_CSV_DASHBOARD_CHART,
+      {
+        slice_id: slice.slice_id,
+        is_cached: chartQuery && chartQuery.is_cached,
+        version: 'v1',
+      },
+      true,
+    );
+    const formData = this.getFormDataExtra(slice);
+    exportChart(formData, 'csv');
+  }
+
+  handleSetEditMode(nextEditMode) {
+    if (this.props.dashboard.forceV2Edit) {
+      this.handleConvertToV2(true);
+    } else {
+      this.props.actions.setEditMode(nextEditMode);
+    }
+  }
+
+  // re-render chart without fetch
+  rerenderCharts() {
+    this.getAllSlices().forEach((slice) => {
+      setTimeout(() => {
+        this.props.actions.renderTriggered(new Date().getTime(), slice.chartKey);
+      }, 50);
+    });
+  }
+
+  render() {
+    const { dashboard, editMode } = this.props;
+    return (
+      <div id="dashboard-container">
+        <div id="dashboard-header">
+          <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.handleSetEditMode}
+          />
+        </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>
+      </div>
+    );
+  }
+}
+
+Dashboard.propTypes = propTypes;
+Dashboard.defaultProps = defaultProps;
+
+export default Dashboard;
diff --git a/superset/assets/src/dashboard/deprecated/v1/components/DashboardContainer.jsx b/superset/assets/src/dashboard/deprecated/v1/components/DashboardContainer.jsx
new file mode 100644
index 0000000..a18a5d2
--- /dev/null
+++ b/superset/assets/src/dashboard/deprecated/v1/components/DashboardContainer.jsx
@@ -0,0 +1,31 @@
+import { bindActionCreators } from 'redux';
+import { connect } from 'react-redux';
+
+import * as dashboardActions from '../actions';
+import * as chartActions from '../../chart/chartAction';
+import Dashboard from './Dashboard';
+
+function mapStateToProps({ charts, dashboard, 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,
+  };
+}
+
+function mapDispatchToProps(dispatch) {
+  const actions = { ...chartActions, ...dashboardActions };
+  return {
+    actions: bindActionCreators(actions, dispatch),
+  };
+}
+
+export default connect(mapStateToProps, mapDispatchToProps)(Dashboard);
diff --git a/superset/assets/src/dashboard/deprecated/v1/components/GridCell.jsx b/superset/assets/src/dashboard/deprecated/v1/components/GridCell.jsx
new file mode 100644
index 0000000..d68b427
--- /dev/null
+++ b/superset/assets/src/dashboard/deprecated/v1/components/GridCell.jsx
@@ -0,0 +1,157 @@
+/* eslint-disable react/no-danger */
+import React from 'react';
+import PropTypes from 'prop-types';
+
+import SliceHeader from './SliceHeader';
+import ChartContainer from '../../chart/ChartContainer';
+
+import '../../../../../stylesheets/dashboard_deprecated.css';
+
+const propTypes = {
+  timeout: PropTypes.number,
+  datasource: PropTypes.object,
+  isLoading: PropTypes.bool,
+  isCached: PropTypes.bool,
+  cachedDttm: PropTypes.string,
+  isExpanded: PropTypes.bool,
+  widgetHeight: PropTypes.number,
+  widgetWidth: PropTypes.number,
+  slice: PropTypes.object,
+  chartKey: PropTypes.string,
+  formData: PropTypes.object,
+  filters: PropTypes.object,
+  forceRefresh: PropTypes.func,
+  removeSlice: 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: () => ({}),
+  updateSliceName: () => ({}),
+  toggleExpandSlice: () => ({}),
+  exploreChart: () => ({}),
+  exportCSV: () => ({}),
+  addFilter: () => ({}),
+  getFilters: () => ({}),
+  clearFilter: () => ({}),
+  removeFilter: () => ({}),
+  editMode: false,
+};
+
+class GridCell extends React.PureComponent {
+  constructor(props) {
+    super(props);
+
+    const sliceId = this.props.slice.slice_id;
+    this.addFilter = this.props.addFilter.bind(this, sliceId);
+    this.getFilters = this.props.getFilters.bind(this, sliceId);
+    this.clearFilter = this.props.clearFilter.bind(this, sliceId);
+    this.removeFilter = this.props.removeFilter.bind(this, sliceId);
+  }
+
+  getDescriptionId(slice) {
+    return 'description_' + slice.slice_id;
+  }
+
+  getHeaderId(slice) {
+    return 'header_' + slice.slice_id;
+  }
+
+  width() {
+    return this.props.widgetWidth - 10;
+  }
+
+  height(slice) {
+    const widgetHeight = this.props.widgetHeight;
+    const headerHeight = this.headerHeight(slice);
+    const descriptionId = this.getDescriptionId(slice);
+    let descriptionHeight = 0;
+    if (this.props.isExpanded && this.refs[descriptionId]) {
+      descriptionHeight = this.refs[descriptionId].offsetHeight + 10;
+    }
+
+    return widgetHeight - headerHeight - descriptionHeight;
+  }
+
+  headerHeight(slice) {
+    const headerId = this.getHeaderId(slice);
+    return this.refs[headerId] ? this.refs[headerId].offsetHeight : 30;
+  }
+
+  render() {
+    const {
+      isExpanded, isLoading, isCached, cachedDttm,
+      removeSlice, updateSliceName, toggleExpandSlice, forceRefresh,
+      chartKey, slice, datasource, formData, timeout, annotationQuery,
+      exploreChart, exportCSV,
+    } = this.props;
+    return (
+      <div
+        className={isLoading ? 'slice-cell-highlight' : 'slice-cell'}
+        id={`${slice.slice_id}-cell`}
+      >
+        <div ref={this.getHeaderId(slice)}>
+          <SliceHeader
+            slice={slice}
+            isExpanded={isExpanded}
+            isCached={isCached}
+            cachedDttm={cachedDttm}
+            removeSlice={removeSlice}
+            updateSliceName={updateSliceName}
+            toggleExpandSlice={toggleExpandSlice}
+            forceRefresh={forceRefresh}
+            editMode={this.props.editMode}
+            annotationQuery={annotationQuery}
+            exploreChart={exploreChart}
+            exportCSV={exportCSV}
+          />
+        </div>
+        {
+        /* This usage of dangerouslySetInnerHTML is safe since it is being used to render
+           markdown that is sanitized with bleach. See:
+             https://github.com/apache/incubator-superset/pull/4390
+           and
+             https://github.com/apache/incubator-superset/commit/b6fcc22d5a2cb7a5e92599ed5795a0169385a825 */}
+        <div
+          className="slice_description bs-callout bs-callout-default"
+          style={isExpanded ? {} : { display: 'none' }}
+          ref={this.getDescriptionId(slice)}
+          dangerouslySetInnerHTML={{ __html: slice.description_markeddown }}
+        />
+        <div className="row chart-container">
+          <input type="hidden" value="false" />
+          <ChartContainer
+            containerId={`slice-container-${slice.slice_id}`}
+            chartKey={chartKey}
+            datasource={datasource}
+            formData={formData}
+            headerHeight={this.headerHeight(slice)}
+            height={this.height(slice)}
+            width={this.width()}
+            timeout={timeout}
+            vizType={slice.formData.viz_type}
+            addFilter={this.addFilter}
+            getFilters={this.getFilters}
+            clearFilter={this.clearFilter}
+            removeFilter={this.removeFilter}
+          />
+        </div>
+      </div>
+    );
+  }
+}
+
+GridCell.propTypes = propTypes;
+GridCell.defaultProps = defaultProps;
+
+export default GridCell;
diff --git a/superset/assets/src/dashboard/deprecated/v1/components/GridLayout.jsx b/superset/assets/src/dashboard/deprecated/v1/components/GridLayout.jsx
new file mode 100644
index 0000000..ef0ec24
--- /dev/null
+++ b/superset/assets/src/dashboard/deprecated/v1/components/GridLayout.jsx
@@ -0,0 +1,198 @@
+import React from 'react';
+import PropTypes from 'prop-types';
+import { Responsive, WidthProvider } from 'react-grid-layout';
+
+import GridCell from './GridCell';
+
+require('react-grid-layout/css/styles.css');
+require('react-resizable/css/styles.css');
+
+const ResponsiveReactGridLayout = WidthProvider(Responsive);
+
+const propTypes = {
+  dashboard: PropTypes.object.isRequired,
+  datasources: PropTypes.object,
+  charts: PropTypes.object.isRequired,
+  filters: PropTypes.object,
+  timeout: PropTypes.number,
+  onChange: 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,
+  toggleExpandSlice: PropTypes.func,
+  addFilter: PropTypes.func,
+  getFilters: PropTypes.func,
+  clearFilter: PropTypes.func,
+  removeFilter: PropTypes.func,
+  editMode: PropTypes.bool.isRequired,
+};
+
+const defaultProps = {
+  onChange: () => ({}),
+  getFormDataExtra: () => ({}),
+  exploreChart: () => ({}),
+  exportCSV: () => ({}),
+  fetchSlice: () => ({}),
+  saveSlice: () => ({}),
+  removeSlice: () => ({}),
+  removeChart: () => ({}),
+  updateDashboardLayout: () => ({}),
+  toggleExpandSlice: () => ({}),
+  addFilter: () => ({}),
+  getFilters: () => ({}),
+  clearFilter: () => ({}),
+  removeFilter: () => ({}),
+};
+
+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.bind(this) : null;
+  }
+
+  onResizeStop(layout) {
+    this.props.updateDashboardLayout(layout);
+    this.props.onChange();
+  }
+
+  onDragStop(layout) {
+    this.props.updateDashboardLayout(layout);
+    this.props.onChange();
+  }
+
+  getWidgetId(slice) {
+    return 'widget_' + slice.slice_id;
+  }
+
+  getWidgetHeight(slice) {
+    const widgetId = this.getWidgetId(slice);
+    if (!widgetId || !this.refs[widgetId]) {
+      return 400;
+    }
+    return this.refs[widgetId].offsetHeight;
+  }
+
+  getWidgetWidth(slice) {
+    const widgetId = this.getWidgetId(slice);
+    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();
+  }
+
+  updateSliceName(sliceId, sliceName) {
+    const index = this.findSliceIndexById(sliceId);
+    if (index === -1) {
+      return;
+    }
+
+    const currentSlice = this.props.dashboard.slices[index];
+    if (currentSlice.slice_name === sliceName) {
+      return;
+    }
+
+    this.props.saveSlice(currentSlice, sliceName);
+  }
+
+  isExpanded(slice) {
+    return this.props.dashboard.metadata.expanded_slices &&
+      this.props.dashboard.metadata.expanded_slices[slice.slice_id];
+  }
+
+  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>);
+    });
+
+    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>
+    );
+  }
+}
+
+GridLayout.propTypes = propTypes;
+GridLayout.defaultProps = defaultProps;
+
+export default GridLayout;
diff --git a/superset/assets/src/dashboard/deprecated/v1/components/Header.jsx b/superset/assets/src/dashboard/deprecated/v1/components/Header.jsx
new file mode 100644
index 0000000..a84ee89
--- /dev/null
+++ b/superset/assets/src/dashboard/deprecated/v1/components/Header.jsx
@@ -0,0 +1,184 @@
+import React from 'react';
+import PropTypes from 'prop-types';
+
+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 PromptV2ConversionModal from '../../PromptV2ConversionModal';
+import {
+  Logger,
+  LOG_ACTIONS_PREVIEW_V2,
+  LOG_ACTIONS_DISMISS_V2_PROMPT,
+  LOG_ACTIONS_SHOW_V2_INFO_PROMPT,
+} from '../../../../logger';
+import { t } from '../../../../locales';
+
+const propTypes = {
+  dashboard: PropTypes.object.isRequired,
+  filters: PropTypes.object.isRequired,
+  userId: PropTypes.string.isRequired,
+  isStarred: PropTypes.bool,
+  addSlicesToDashboard: PropTypes.func,
+  onSave: PropTypes.func,
+  onChange: PropTypes.func,
+  fetchFaveStar: PropTypes.func,
+  renderSlices: PropTypes.func,
+  saveFaveStar: PropTypes.func,
+  serialize: PropTypes.func,
+  startPeriodicRender: PropTypes.func,
+  updateDashboardTitle: PropTypes.func,
+  editMode: PropTypes.bool.isRequired,
+  setEditMode: PropTypes.func.isRequired,
+  unsavedChanges: PropTypes.bool.isRequired,
+};
+
+class Header extends React.PureComponent {
+  constructor(props) {
+    super(props);
+    this.handleSaveTitle = this.handleSaveTitle.bind(this);
+    this.toggleEditMode = this.toggleEditMode.bind(this);
+    this.state = {
+      showV2PromptModal: props.dashboard.promptV2Conversion,
+    };
+    this.toggleShowV2PromptModal = this.toggleShowV2PromptModal.bind(this);
+    this.handleConvertToV2 = this.handleConvertToV2.bind(this);
+  }
+  handleSaveTitle(title) {
+    this.props.updateDashboardTitle(title);
+  }
+  handleConvertToV2(editMode) {
+    Logger.append(
+      LOG_ACTIONS_PREVIEW_V2,
+      {
+        force_v2_edit: this.props.dashboard.forceV2Edit,
+        edit_mode: editMode === true,
+      },
+      true,
+    );
+    const url = new URL(window.location); // eslint-disable-line
+    url.searchParams.set('version', 'v2');
+    if (editMode === true) url.searchParams.set('edit', true);
+    window.location = url; // eslint-disable-line
+  }
+  toggleEditMode() {
+    this.props.setEditMode(!this.props.editMode);
+  }
+  toggleShowV2PromptModal() {
+    const nextShowModal = !this.state.showV2PromptModal;
+    this.setState({ showV2PromptModal: nextShowModal });
+    if (nextShowModal) {
+      Logger.append(
+        LOG_ACTIONS_SHOW_V2_INFO_PROMPT,
+        {
+          force_v2_edit: this.props.dashboard.forceV2Edit,
+        },
+        true,
+      );
+    } else {
+      Logger.append(
+        LOG_ACTIONS_DISMISS_V2_PROMPT,
+        {
+          force_v2_edit: this.props.dashboard.forceV2Edit,
+        },
+        true,
+      );
+    }
+  }
+  renderUnsaved() {
+    if (!this.props.unsavedChanges) {
+      return null;
+    }
+    return (
+      <InfoTooltipWithTrigger
+        label="unsaved"
+        tooltip={t('Unsaved changes')}
+        icon="exclamation-triangle"
+        className="text-danger m-r-5"
+        placement="top"
+      />
+    );
+  }
+  renderEditButton() {
+    if (!this.props.dashboard.dash_save_perm) {
+      return null;
+    }
+    const btnText = this.props.editMode ? 'Switch to View Mode' : 'Edit Dashboard';
+    return (
+      <Button
+        bsStyle="default"
+        className="m-r-5"
+        style={{ width: '150px' }}
+        onClick={this.toggleEditMode}
+      >
+        {btnText}
+      </Button>);
+  }
+  render() {
+    const dashboard = this.props.dashboard;
+    return (
+      <div className="title">
+        <div className="pull-left">
+          <h1 className="outer-container pull-left">
+            <EditableTitle
+              title={dashboard.dashboard_title}
+              canEdit={dashboard.dash_save_perm && this.props.editMode}
+              onSaveTitle={this.handleSaveTitle}
+              showTooltip={this.props.editMode}
+            />
+            <span className="favstar m-l-5">
+              <FaveStar
+                itemId={dashboard.id}
+                fetchFaveStar={this.props.fetchFaveStar}
+                saveFaveStar={this.props.saveFaveStar}
+                isStarred={this.props.isStarred}
+              />
+            </span>
+            {dashboard.promptV2Conversion && (
+              <span
+                role="none"
+                className="v2-preview-badge"
+                onClick={this.toggleShowV2PromptModal}
+              >
+                {t('Convert to v2')}
+                <span className="fa fa-info-circle m-l-5" />
+              </span>
+            )}
+            {this.renderUnsaved()}
+          </h1>
+        </div>
+        <div className="pull-right" style={{ marginTop: '35px' }}>
+          {this.renderEditButton()}
+          <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}
+            startPeriodicRender={this.props.startPeriodicRender}
+            editMode={this.props.editMode}
+          />
+        </div>
+        <div className="clearfix" />
+        {this.state.showV2PromptModal &&
+          dashboard.promptV2Conversion &&
+          !this.props.editMode && (
+            <PromptV2ConversionModal
+              onClose={this.toggleShowV2PromptModal}
+              handleConvertToV2={this.handleConvertToV2}
+              forceV2Edit={dashboard.forceV2Edit}
+              v2AutoConvertDate={dashboard.v2AutoConvertDate}
+              v2FeedbackUrl={dashboard.v2FeedbackUrl}
+            />
+          )}
+      </div>
+    );
+  }
+}
+Header.propTypes = propTypes;
+
+export default Header;
diff --git a/superset/assets/src/dashboard/deprecated/v1/components/RefreshIntervalModal.jsx b/superset/assets/src/dashboard/deprecated/v1/components/RefreshIntervalModal.jsx
new file mode 100644
index 0000000..3e43f93
--- /dev/null
+++ b/superset/assets/src/dashboard/deprecated/v1/components/RefreshIntervalModal.jsx
@@ -0,0 +1,64 @@
+import React from 'react';
+import PropTypes from 'prop-types';
+import Select from 'react-select';
+import ModalTrigger from '../../../../components/ModalTrigger';
+import { t } from '../../../../locales';
+
+const propTypes = {
+  triggerNode: PropTypes.node.isRequired,
+  initialRefreshFrequency: PropTypes.number,
+  onChange: PropTypes.func,
+};
+
+const defaultProps = {
+  initialRefreshFrequency: 0,
+  onChange: () => {},
+};
+
+const options = [
+  [0, t('Don\'t refresh')],
+  [10, t('10 seconds')],
+  [30, t('30 seconds')],
+  [60, t('1 minute')],
+  [300, t('5 minutes')],
+  [1800, t('30 minutes')],
+  [3600, t('1 hour')],
+  [21600, t('6 hours')],
+  [43200, t('12 hours')],
+  [86400, t('24 hours')],
+].map(o => ({ value: o[0], label: o[1] }));
+
+class RefreshIntervalModal extends React.PureComponent {
+  constructor(props) {
+    super(props);
+    this.state = {
+      refreshFrequency: props.initialRefreshFrequency,
+    };
+  }
+  render() {
+    return (
+      <ModalTrigger
+        triggerNode={this.props.triggerNode}
+        isMenuItem
+        modalTitle={t('Refresh Interval')}
+        modalBody={
+          <div>
+            {t('Choose the refresh frequency for this dashboard')}
+            <Select
+              options={options}
+              value={this.state.refreshFrequency}
+              onChange={(opt) => {
+                this.setState({ refreshFrequency: opt.value });
+                this.props.onChange(opt.value);
+              }}
+            />
+          </div>
+        }
+      />
+    );
+  }
+}
+RefreshIntervalModal.propTypes = propTypes;
+RefreshIntervalModal.defaultProps = defaultProps;
+
+export default RefreshIntervalModal;
diff --git a/superset/assets/src/dashboard/deprecated/v1/components/SaveModal.jsx b/superset/assets/src/dashboard/deprecated/v1/components/SaveModal.jsx
new file mode 100644
index 0000000..aa622ab
--- /dev/null
+++ b/superset/assets/src/dashboard/deprecated/v1/components/SaveModal.jsx
@@ -0,0 +1,161 @@
+/* global notify */
+import React from 'react';
+import PropTypes from 'prop-types';
+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,
+  triggerNode: PropTypes.node.isRequired,
+  filters: PropTypes.object.isRequired,
+  serialize: PropTypes.func,
+  onSave: PropTypes.func,
+};
+
+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]',
+      duplicateSlices: false,
+    };
+    this.modal = null;
+    this.handleSaveTypeChange = this.handleSaveTypeChange.bind(this);
+    this.handleNameChange = this.handleNameChange.bind(this);
+    this.saveDashboard = this.saveDashboard.bind(this);
+  }
+  toggleDuplicateSlices() {
+    this.setState({ duplicateSlices: !this.state.duplicateSlices });
+  }
+  handleSaveTypeChange(event) {
+    this.setState({
+      saveType: event.target.value,
+    });
+  }
+  handleNameChange(event) {
+    this.setState({
+      newDashName: event.target.value,
+      saveType: 'newDashboard',
+    });
+  }
+  saveDashboardRequest(data, url, saveType) {
+    const saveModal = this.modal;
+    const onSaveDashboard = this.props.onSave;
+    Object.assign(data, { css: this.props.css });
+    $.ajax({
+      type: 'POST',
+      url,
+      data: {
+        data: JSON.stringify(data),
+      },
+      success(resp) {
+        saveModal.close();
+        onSaveDashboard();
+        if (saveType === 'newDashboard') {
+          window.location = `/superset/dashboard/${resp.id}/`;
+        } else {
+          notify.success(t('This dashboard was saved successfully.'));
+        }
+      },
+      error(error) {
+        saveModal.close();
+        const errorMsg = getAjaxErrorMsg(error);
+        notify.error(t('Sorry, there was an error saving this dashboard: ') + errorMsg);
+      },
+    });
+  }
+  saveDashboard(saveType, newDashboardTitle) {
+    const dashboard = this.props.dashboard;
+    const positions = this.props.serialize();
+    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),
+      duplicate_slices: this.state.duplicateSlices,
+    };
+    let url = null;
+    if (saveType === 'overwrite') {
+      url = `/superset/save_dash/${dashboard.id}/`;
+      this.saveDashboardRequest(data, url, saveType);
+    } else if (saveType === 'newDashboard') {
+      if (!newDashboardTitle) {
+        this.modal.close();
+        showModal({
+          title: t('Error'),
+          body: t('You must pick a name for the new dashboard'),
+        });
+      } else {
+        data.dashboard_title = newDashboardTitle;
+        url = `/superset/copy_dash/${dashboard.id}/`;
+        this.saveDashboardRequest(data, url, saveType);
+      }
+    }
+  }
+  render() {
+    return (
+      <ModalTrigger
+        ref={(modal) => { this.modal = modal; }}
+        isMenuItem
+        triggerNode={this.props.triggerNode}
+        modalTitle={t('Save Dashboard')}
+        modalBody={
+          <FormGroup>
+            <Radio
+              value="overwrite"
+              onChange={this.handleSaveTypeChange}
+              checked={this.state.saveType === 'overwrite'}
+            >
+              {t('Overwrite Dashboard [%s]', this.props.dashboard.dashboard_title)}
+            </Radio>
+            <hr />
+            <Radio
+              value="newDashboard"
+              onChange={this.handleSaveTypeChange}
+              checked={this.state.saveType === 'newDashboard'}
+            >
+              {t('Save as:')}
+            </Radio>
+            <FormControl
+              type="text"
+              placeholder={t('[dashboard name]')}
+              value={this.state.newDashName}
+              onFocus={this.handleNameChange}
+              onChange={this.handleNameChange}
+            />
+            <div className="m-l-25 m-t-5">
+              <Checkbox
+                checked={this.state.duplicateSlices}
+                onChange={this.toggleDuplicateSlices.bind(this)}
+              />
+              <span className="m-l-5">also copy (duplicate) charts</span>
+            </div>
+          </FormGroup>
+        }
+        modalFooter={
+          <div>
+            <Button
+              bsStyle="primary"
+              onClick={() => { this.saveDashboard(this.state.saveType, this.state.newDashName); }}
+            >
+              {t('Save')}
+            </Button>
+          </div>
+        }
+      />
+    );
+  }
+}
+SaveModal.propTypes = propTypes;
+
+export default SaveModal;
diff --git a/superset/assets/src/dashboard/deprecated/v1/components/SliceAdder.jsx b/superset/assets/src/dashboard/deprecated/v1/components/SliceAdder.jsx
new file mode 100644
index 0000000..6c2f624
--- /dev/null
+++ b/superset/assets/src/dashboard/deprecated/v1/components/SliceAdder.jsx
@@ -0,0 +1,219 @@
+import React from 'react';
+import $ from 'jquery';
+import PropTypes from 'prop-types';
+import { BootstrapTable, TableHeaderColumn } from 'react-bootstrap-table';
+
+import ModalTrigger from '../../../../components/ModalTrigger';
+import { t } from '../../../../locales';
+
+require('react-bootstrap-table/css/react-bootstrap-table.css');
+
+const propTypes = {
+  dashboard: PropTypes.object.isRequired,
+  triggerNode: PropTypes.node.isRequired,
+  userId: PropTypes.string.isRequired,
+  addSlicesToDashboard: PropTypes.func,
+};
+
+class SliceAdder extends React.Component {
+  constructor(props) {
+    super(props);
+    this.state = {
+      slices: [],
+      slicesLoaded: false,
+      selectionMap: {},
+    };
+
+    this.options = {
+      defaultSortOrder: 'desc',
+      defaultSortName: 'modified',
+      sizePerPage: 10,
+    };
+
+    this.addSlices = this.addSlices.bind(this);
+    this.toggleSlice = this.toggleSlice.bind(this);
+
+    this.selectRowProp = {
+      mode: 'checkbox',
+      clickToSelect: true,
+      onSelect: this.toggleSlice,
+    };
+  }
+
+  componentWillUnmount() {
+    if (this.slicesRequest) {
+      this.slicesRequest.abort();
+    }
+  }
+
+  onEnterModal() {
+    const uri = `/sliceaddview/api/read?_flt_0_created_by=${this.props.userId}`;
+    this.slicesRequest = $.ajax({
+      url: uri,
+      type: 'GET',
+      success: (response) => {
+        // Prepare slice data for table
+        const slices = response.result.map(slice => ({
+          id: slice.id,
+          sliceName: slice.slice_name,
+          vizType: slice.viz_type,
+          datasourceLink: slice.datasource_link,
+          modified: slice.modified,
+        }));
+
+        this.setState({
+          slices,
+          selectionMap: {},
+          slicesLoaded: true,
+        });
+      },
+      error: (error) => {
+        this.errored = true;
+        this.setState({
+          errorMsg: t('Sorry, there was an error fetching charts to this dashboard: ') +
+          this.getAjaxErrorMsg(error),
+        });
+      },
+    });
+  }
+
+  getAjaxErrorMsg(error) {
+    const respJSON = error.responseJSON;
+    return (respJSON && respJSON.message) ? respJSON.message :
+      error.responseText;
+  }
+
+  addSlices() {
+    const adder = this;
+    this.props.addSlicesToDashboard(Object.keys(this.state.selectionMap))
+      // if successful, page will be reloaded.
+      .fail((error) => {
+        adder.errored = true;
+        adder.setState({
+          errorMsg: t('Sorry, there was an error adding charts to this dashboard: ') +
+          this.getAjaxErrorMsg(error),
+        });
+      });
+  }
+
+  toggleSlice(slice) {
+    const selectionMap = Object.assign({}, this.state.selectionMap);
+    selectionMap[slice.id] = !selectionMap[slice.id];
+    this.setState({ selectionMap });
+  }
+
+  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;
+    }
+
+    if (a.changed_on < b.changed_on) {
+      return -1;
+    } else if (a.changed_on > b.changed_on) {
+      return 1;
+    }
+    return 0;
+  }
+
+  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"
+          >
+            <TableHeaderColumn
+              dataField="id"
+              isKey
+              dataSort
+              hidden
+            />
+            <TableHeaderColumn
+              dataField="sliceName"
+              dataSort
+            >
+              {t('Name')}
+            </TableHeaderColumn>
+            <TableHeaderColumn
+              dataField="vizType"
+              dataSort
+            >
+              {t('Viz')}
+            </TableHeaderColumn>
+            <TableHeaderColumn
+              dataField="datasourceLink"
+              dataSort
+              // Will cause react-bootstrap-table to interpret the HTML returned
+              dataFormat={datasourceLink => datasourceLink}
+            >
+              {t('Datasource')}
+            </TableHeaderColumn>
+            <TableHeaderColumn
+              dataField="modified"
+              dataSort
+              sortFunc={this.modifiedDateComparator}
+              // Will cause react-bootstrap-table to interpret the HTML returned
+              dataFormat={modified => modified}
+            >
+              {t('Modified')}
+            </TableHeaderColumn>
+          </BootstrapTable>
+          <button
+            type="button"
+            className="btn btn-default"
+            data-dismiss="modal"
+            onClick={this.addSlices}
+            disabled={!enableAddSlice}
+          >
+            {t('Add Charts')}
+          </button>
+        </div>
+      </div>
+    );
+
+    return (
+      <ModalTrigger
+        triggerNode={this.props.triggerNode}
+        tooltip={t('Add a new chart to the dashboard')}
+        beforeOpen={this.onEnterModal.bind(this)}
+        isMenuItem
+        modalBody={modalContent}
+        bsSize="large"
+        setModalAsTriggerChildren
+        modalTitle={t('Add Charts to Dashboard')}
+      />
+    );
+  }
+}
+
+SliceAdder.propTypes = propTypes;
+
+export default SliceAdder;
diff --git a/superset/assets/src/dashboard/deprecated/v1/components/SliceHeader.jsx b/superset/assets/src/dashboard/deprecated/v1/components/SliceHeader.jsx
new file mode 100644
index 0000000..a8c6aa7
--- /dev/null
+++ b/superset/assets/src/dashboard/deprecated/v1/components/SliceHeader.jsx
@@ -0,0 +1,194 @@
+import React from 'react';
+import PropTypes from 'prop-types';
+import moment from 'moment';
+import { connect } from 'react-redux';
+
+import { t } from '../../../../locales';
+import EditableTitle from '../../../../components/EditableTitle';
+import TooltipWrapper from '../../../../components/TooltipWrapper';
+
+const propTypes = {
+  slice: PropTypes.object.isRequired,
+  supersetCanExplore: PropTypes.bool,
+  sliceCanEdit: PropTypes.bool,
+  isExpanded: PropTypes.bool,
+  isCached: PropTypes.bool,
+  cachedDttm: PropTypes.string,
+  removeSlice: PropTypes.func,
+  updateSliceName: PropTypes.func,
+  toggleExpandSlice: PropTypes.func,
+  forceRefresh: PropTypes.func,
+  exploreChart: PropTypes.func,
+  exportCSV: PropTypes.func,
+  editMode: PropTypes.bool,
+  annotationQuery: PropTypes.object,
+  annotationError: PropTypes.object,
+};
+
+const defaultProps = {
+  forceRefresh: () => ({}),
+  removeSlice: () => ({}),
+  updateSliceName: () => ({}),
+  toggleExpandSlice: () => ({}),
+  exploreChart: () => ({}),
+  exportCSV: () => ({}),
+  editMode: false,
+};
+
+class SliceHeader extends React.PureComponent {
+  constructor(props) {
+    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) {
+    if (this.props.updateSliceName) {
+      this.props.updateSliceName(this.props.slice.slice_id, newTitle);
+    }
+  }
+
+  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 annoationsLoading = t('Annotation layers are still loading.');
+    const annoationsError = t('One ore more annotation layers failed loading.');
+
+    return (
+      <div className="row chart-header">
+        <div className="col-md-12">
+          <div className="header">
+            <EditableTitle
+              title={slice.slice_name}
+              canEdit={!!this.props.updateSliceName && this.props.editMode}
+              onSaveTitle={this.onSaveTitle}
+              showTooltip={this.props.editMode}
+              noPermitTooltip={'You don\'t have the rights to alter this dashboard.'}
+            />
+            {!!Object.values(this.props.annotationQuery || {}).length &&
+              <TooltipWrapper
+                label="annotations-loading"
+                placement="top"
+                tooltip={annoationsLoading}
+              >
+                <i className="fa fa-refresh warning" />
+              </TooltipWrapper>
+            }
+            {!!Object.values(this.props.annotationError || {}).length &&
+              <TooltipWrapper
+                label="annoation-errors"
+                placement="top"
+                tooltip={annoationsError}
+              >
+                <i className="fa fa-exclamation-circle danger" />
+              </TooltipWrapper>
+            }
+          </div>
+          <div className="chart-controls">
+            <div id={'controls_' + slice.slice_id} className="pull-right">
+              {this.props.editMode &&
+                <a>
+                  <TooltipWrapper
+                    placement="top"
+                    label="move"
+                    tooltip={t('Move chart')}
+                  >
+                    <i className="fa fa-arrows drag" />
+                  </TooltipWrapper>
+                </a>
+              }
+              <a className={`refresh ${isCached ? 'danger' : ''}`} onClick={this.forceRefresh}>
+                <TooltipWrapper
+                  placement="top"
+                  label="refresh"
+                  tooltip={refreshTooltip}
+                >
+                  <i className="fa fa-repeat" />
+                </TooltipWrapper>
+              </a>
+              {slice.description &&
+              <a onClick={this.onToggleExpandSlice}>
+                <TooltipWrapper
+                  placement="top"
+                  label="description"
+                  tooltip={t('Toggle chart description')}
+                >
+                  <i className="fa fa-info-circle slice_info" />
+                </TooltipWrapper>
+              </a>
+              }
+              {this.props.sliceCanEdit &&
+                <a href={slice.edit_url} target="_blank">
+                  <TooltipWrapper
+                    placement="top"
+                    label="edit"
+                    tooltip={t('Edit chart')}
+                  >
+                    <i className="fa fa-pencil" />
+                  </TooltipWrapper>
+                </a>
+              }
+              <a className="exportCSV" onClick={this.exportCSV}>
+                <TooltipWrapper
+                  placement="top"
+                  label="exportCSV"
+                  tooltip={t('Export CSV')}
+                >
+                  <i className="fa fa-table" />
+                </TooltipWrapper>
+              </a>
+              {this.props.supersetCanExplore &&
+                <a className="exploreChart" onClick={this.exploreChart}>
+                  <TooltipWrapper
+                    placement="top"
+                    label="exploreChart"
+                    tooltip={t('Explore chart')}
+                  >
+                    <i className="fa fa-share" />
+                  </TooltipWrapper>
+                </a>
+              }
+              {this.props.editMode &&
+                <a className="remove-chart" onClick={this.removeSlice}>
+                  <TooltipWrapper
+                    placement="top"
+                    label="close"
+                    tooltip={t('Remove chart from dashboard')}
+                  >
+                    <i className="fa fa-close" />
+                  </TooltipWrapper>
+                </a>
+              }
+            </div>
+          </div>
+        </div>
+      </div>
+    );
+  }
+}
+
+SliceHeader.propTypes = propTypes;
+SliceHeader.defaultProps = defaultProps;
+
+function mapStateToProps({ dashboard }) {
+  return {
+    supersetCanExplore: dashboard.dashboard.superset_can_explore,
+    sliceCanEdit: dashboard.dashboard.slice_can_edit,
+  };
+}
+
+export { SliceHeader };
+export default connect(mapStateToProps, () => ({}))(SliceHeader);
diff --git a/superset/assets/src/dashboard/deprecated/v1/index.jsx b/superset/assets/src/dashboard/deprecated/v1/index.jsx
new file mode 100644
index 0000000..d7e898e
--- /dev/null
+++ b/superset/assets/src/dashboard/deprecated/v1/index.jsx
@@ -0,0 +1,28 @@
+import React from 'react';
+import ReactDOM from 'react-dom';
+import { createStore, applyMiddleware, compose } from 'redux';
+import { Provider } from 'react-redux';
+import thunk from 'redux-thunk';
+
+import { initEnhancer } from '../../../reduxUtils';
+import { appSetup } from '../../../common';
+import { initJQueryAjax } from '../../../modules/utils';
+import DashboardContainer from './components/DashboardContainer';
+import rootReducer, { getInitialState } from './reducers';
+
+appSetup();
+initJQueryAjax();
+
+const appContainer = document.getElementById('app');
+const bootstrapData = JSON.parse(appContainer.getAttribute('data-bootstrap'));
+const initState = Object.assign({}, getInitialState(bootstrapData));
+
+const store = createStore(
+  rootReducer, initState, compose(applyMiddleware(thunk), initEnhancer(false)));
+
+ReactDOM.render(
+  <Provider store={store}>
+    <DashboardContainer />
+  </Provider>,
+  appContainer,
+);
diff --git a/superset/assets/src/dashboard/deprecated/v1/reducers.js b/superset/assets/src/dashboard/deprecated/v1/reducers.js
new file mode 100644
index 0000000..00bf2bf
--- /dev/null
+++ b/superset/assets/src/dashboard/deprecated/v1/reducers.js
@@ -0,0 +1,272 @@
+/* eslint-disable camelcase */
+import { combineReducers } from 'redux';
+import d3 from 'd3';
+import shortid from 'shortid';
+
+import charts, { chart } from '../chart/chartReducer';
+import * as actions from './actions';
+import { getParam } from '../../../modules/utils';
+import { alterInArr, removeFromArr } from '../../../reduxUtils';
+import { applyDefaultFormData } from '../../../explore/store';
+import { getColorFromScheme } from '../../../modules/colors';
+
+export function getInitialState(bootstrapData) {
+  const {
+    user_id,
+    datasources,
+    common,
+    editMode,
+    prompt_v2_conversion,
+    force_v2_edit,
+    v2_auto_convert_date,
+    v2_feedback_url,
+  } = bootstrapData;
+  delete common.locale;
+  delete common.language_pack;
+
+  const dashboard = {
+    ...bootstrapData.dashboard_data,
+    promptV2Conversion: prompt_v2_conversion,
+    forceV2Edit: force_v2_edit,
+    v2AutoConvertDate: v2_auto_convert_date,
+    v2FeedbackUrl: v2_feedback_url,
+  };
+  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/dashboardLayout.js b/superset/assets/src/dashboard/reducers/dashboardLayout.js
index 4b3ee49..396a56c 100644
--- a/superset/assets/src/dashboard/reducers/dashboardLayout.js
+++ b/superset/assets/src/dashboard/reducers/dashboardLayout.js
@@ -1,21 +1,14 @@
 import {
   DASHBOARD_ROOT_ID,
   DASHBOARD_GRID_ID,
-  GRID_MIN_COLUMN_COUNT,
   NEW_COMPONENTS_SOURCE_ID,
 } from '../util/constants';
+import findParentId from '../util/findParentId';
 import newComponentFactory from '../util/newComponentFactory';
 import newEntitiesFromDrop from '../util/newEntitiesFromDrop';
 import reorderItem from '../util/dnd-reorder';
 import shouldWrapChildInRow from '../util/shouldWrapChildInRow';
-import {
-  CHART_TYPE,
-  COLUMN_TYPE,
-  MARKDOWN_TYPE,
-  ROW_TYPE,
-  TAB_TYPE,
-  TABS_TYPE,
-} from '../util/componentTypes';
+import { ROW_TYPE, TAB_TYPE, TABS_TYPE } from '../util/componentTypes';
 
 import {
   UPDATE_COMPONENTS,
@@ -46,7 +39,6 @@ const actionHandlers = {
 
     const nextComponents = { ...state };
 
-    // recursively find children to remove
     function recursivelyDeleteChildren(componentId, componentParentId) {
       // delete child and it's children
       const component = nextComponents[componentId];
@@ -73,6 +65,14 @@ const actionHandlers = {
     }
 
     recursivelyDeleteChildren(id, parentId);
+    const nextParent = nextComponents[parentId];
+    if (nextParent.type === ROW_TYPE && nextParent.children.length === 0) {
+      const grandparentId = findParentId({
+        childId: parentId,
+        layout: nextComponents,
+      });
+      recursivelyDeleteChildren(parentId, grandparentId);
+    }
 
     return nextComponents;
   },
@@ -81,28 +81,8 @@ const actionHandlers = {
     const {
       payload: { dropResult },
     } = action;
-    const { destination, dragging } = dropResult;
-    const newEntities = newEntitiesFromDrop({ dropResult, layout: state });
 
-    // if column is a parent, set any resizable children to have a minimum width so that
-    // the chances that they are validly movable to future containers is maximized
-    if (
-      destination.type === COLUMN_TYPE &&
-      [CHART_TYPE, MARKDOWN_TYPE].includes(dragging.type)
-    ) {
-      const newEntitiesArray = Object.values(newEntities);
-      const component = newEntitiesArray.find(
-        entity => entity.type === dragging.type,
-      );
-
-      newEntities[component.id] = {
-        ...component,
-        meta: {
-          ...component.meta,
-          width: GRID_MIN_COLUMN_COUNT,
-        },
-      };
-    }
+    const newEntities = newEntitiesFromDrop({ dropResult, layout: state });
 
     return {
       ...state,
@@ -139,22 +119,6 @@ const actionHandlers = {
       nextEntities[newRow.id] = newRow;
     }
 
-    // if column is a parent, set any resizable children to have a minimum width so that
-    // the chances that they are validly movable to future containers is maximized
-    if (
-      destination.type === COLUMN_TYPE &&
-      [CHART_TYPE, MARKDOWN_TYPE].includes(dragging.type)
-    ) {
-      const component = nextEntities[dragging.id];
-      nextEntities[dragging.id] = {
-        ...component,
-        meta: {
-          ...component.meta,
-          width: GRID_MIN_COLUMN_COUNT,
-        },
-      };
-    }
-
     return {
       ...state,
       ...nextEntities,
diff --git a/superset/assets/src/dashboard/reducers/dashboardState.js b/superset/assets/src/dashboard/reducers/dashboardState.js
index 410ecc0..5312de2 100644
--- a/superset/assets/src/dashboard/reducers/dashboardState.js
+++ b/superset/assets/src/dashboard/reducers/dashboardState.js
@@ -82,6 +82,8 @@ export default function dashboardStateReducer(state = {}, action) {
         ...state,
         hasUnsavedChanges: false,
         maxUndoHistoryExceeded: false,
+        editMode: false,
+        isV2Preview: false, // @TODO remove upon v1 deprecation
       };
     },
 
diff --git a/superset/assets/src/dashboard/reducers/getInitialState.js b/superset/assets/src/dashboard/reducers/getInitialState.js
index d56e480..f4e091e 100644
--- a/superset/assets/src/dashboard/reducers/getInitialState.js
+++ b/superset/assets/src/dashboard/reducers/getInitialState.js
@@ -11,7 +11,15 @@ import { DASHBOARD_VERSION_KEY, DASHBOARD_HEADER_ID } from '../util/constants';
 import { DASHBOARD_HEADER_TYPE, CHART_TYPE } from '../util/componentTypes';
 
 export default function(bootstrapData) {
-  const { user_id, datasources, common } = bootstrapData;
+  const {
+    user_id,
+    datasources,
+    common,
+    editMode,
+    force_v2_edit: forceV2Edit,
+    v2_auto_convert_date: v2AutoConvertDate,
+    v2_feedback_url: v2FeedbackUrl,
+  } = bootstrapData;
   delete common.locale;
   delete common.language_pack;
 
@@ -37,11 +45,10 @@ export default function(bootstrapData) {
 
   // dashboard layout
   const { position_json: positionJson } = dashboard;
+  const shouldConvertToV2 =
+    !positionJson || positionJson[DASHBOARD_VERSION_KEY] !== 'v2';
 
-  const layout =
-    !positionJson || positionJson[DASHBOARD_VERSION_KEY] !== 'v2'
-      ? layoutConverter(dashboard)
-      : positionJson;
+  const layout = shouldConvertToV2 ? layoutConverter(dashboard) : positionJson;
 
   // store the header as a layout component so we can undo/redo changes
   layout[DASHBOARD_HEADER_ID] = {
@@ -107,8 +114,8 @@ export default function(bootstrapData) {
     datasources,
     sliceEntities: { ...initSliceEntities, slices, isLoading: false },
     charts: chartQueries,
+    // read-only data
     dashboardInfo: {
-      // read-only data
       id: dashboard.id,
       slug: dashboard.slug,
       metadata: {
@@ -124,6 +131,9 @@ export default function(bootstrapData) {
       superset_can_explore: dashboard.superset_can_explore,
       slice_can_edit: dashboard.slice_can_edit,
       common,
+      v2AutoConvertDate,
+      v2FeedbackUrl,
+      forceV2Edit,
     },
     dashboardState: {
       sliceIds: Array.from(sliceIds),
@@ -131,10 +141,11 @@ export default function(bootstrapData) {
       filters,
       expandedSlices: dashboard.metadata.expanded_slices || {},
       css: dashboard.css || '',
-      editMode: false,
-      showBuilderPane: false,
+      editMode: dashboard.dash_edit_perm && editMode,
+      showBuilderPane: dashboard.dash_edit_perm && editMode,
       hasUnsavedChanges: false,
       maxUndoHistoryExceeded: false,
+      isV2Preview: shouldConvertToV2,
     },
     dashboardLayout,
     messageToasts: [],
diff --git a/superset/assets/src/dashboard/stylesheets/builder-sidepane.less b/superset/assets/src/dashboard/stylesheets/builder-sidepane.less
index 5f87d0c..bbcb7e1 100644
--- a/superset/assets/src/dashboard/stylesheets/builder-sidepane.less
+++ b/superset/assets/src/dashboard/stylesheets/builder-sidepane.less
@@ -73,23 +73,23 @@
   }
 
   .chart-card-container {
-    padding: 16px;
-
     .chart-card {
       border: 1px solid @gray-light;
+      font-weight: 200;
       height: 120px;
       padding: 16px;
+      margin: 16px;
+      position: relative;
       cursor: move;
-    }
 
-    .chart-card.is-selected {
-      opacity: 0.45;
-      cursor: not-allowed;
+      &:hover {
+        background: @gray-bg;
+      }
     }
 
     .card-title {
       margin-bottom: 8px;
-      font-weight: bold;
+      font-weight: 800;
     }
 
     .card-body {
@@ -98,12 +98,31 @@
 
       .item {
         height: 18px;
-      }
 
-      label {
-        margin-right: 5px;
+        span:first-child {
+          font-weight: 400;
+        }
       }
     }
+
+    .chart-card.is-selected {
+      cursor: not-allowed;
+      opacity: 0.4;
+    }
+
+    .is-added-label {
+      background: @almost-black;
+      color: white;
+      font-size: 12px;
+      line-height: 1em;
+      text-transform: uppercase;
+      position: absolute;
+      padding: 4px 8px;
+      position: absolute;
+      top: 32px;
+      right: 32px;
+      pointer-events: none;
+    }
   }
 
   .slice-adder-container {
@@ -114,7 +133,7 @@
       /* the input is wrapped in a div */
       .search-input {
         flex-grow: 1;
-        margin-left: 16px;
+        margin-right: 16px;
       }
 
       .dropdown.btn-group button,
diff --git a/superset/assets/src/dashboard/stylesheets/builder.less b/superset/assets/src/dashboard/stylesheets/builder.less
index ecf192e..e93c6db 100644
--- a/superset/assets/src/dashboard/stylesheets/builder.less
+++ b/superset/assets/src/dashboard/stylesheets/builder.less
@@ -1,7 +1,6 @@
 .dashboard {
   position: relative;
   color: @almost-black;
-  margin-top: -20px;
 }
 
 .dashboard-header {
diff --git a/superset/assets/src/dashboard/stylesheets/components/markdown.less b/superset/assets/src/dashboard/stylesheets/components/markdown.less
index d377c68..2cfd929 100644
--- a/superset/assets/src/dashboard/stylesheets/components/markdown.less
+++ b/superset/assets/src/dashboard/stylesheets/components/markdown.less
@@ -1,6 +1,11 @@
 .dashboard-markdown {
   overflow: hidden;
 
+  .dashboard-component-chart-holder {
+    overflow-y: auto;
+    overflow-x: hidden;
+  }
+
   .dashboard--editing & {
     cursor: move;
   }
@@ -8,4 +13,4 @@
   #brace-editor {
     border: none;
   }
-}
\ No newline at end of file
+}
diff --git a/superset/assets/src/dashboard/stylesheets/components/new-component.less b/superset/assets/src/dashboard/stylesheets/components/new-component.less
index decb1ad..b330f79 100644
--- a/superset/assets/src/dashboard/stylesheets/components/new-component.less
+++ b/superset/assets/src/dashboard/stylesheets/components/new-component.less
@@ -8,13 +8,17 @@
   cursor: move;
 }
 
+.new-component:not(.static):hover {
+  background: @gray-bg;
+}
+
 .new-component-placeholder {
   position: relative;
   background: @gray-bg;
   width: 40px;
   height: 40px;
   margin-right: 16px;
-  box-shadow: 0 0 1px white;
+  border: 1px solid white;
   display: flex;
   align-items: center;
   justify-content: center;
diff --git a/superset/assets/src/dashboard/stylesheets/components/tabs.less b/superset/assets/src/dashboard/stylesheets/components/tabs.less
index 02039b4..b1124da 100644
--- a/superset/assets/src/dashboard/stylesheets/components/tabs.less
+++ b/superset/assets/src/dashboard/stylesheets/components/tabs.less
@@ -8,6 +8,10 @@
   margin-top: 1px;
 }
 
+.dashboard-component-tabs-content .empty-tab-droptarget {
+  min-height: 24px;
+}
+
 .dashboard-component-tabs .nav-tabs {
   border-bottom: none;
 }
diff --git a/superset/assets/src/dashboard/stylesheets/dashboard.less b/superset/assets/src/dashboard/stylesheets/dashboard.less
index 5756786..3db5cdc 100644
--- a/superset/assets/src/dashboard/stylesheets/dashboard.less
+++ b/superset/assets/src/dashboard/stylesheets/dashboard.less
@@ -1,3 +1,40 @@
+/* header has mysterious extra margin */
+header.top {
+  margin-bottom: -20px;
+}
+
+body {
+  h1 {
+    font-weight: 600;
+    line-height: normal;
+    font-size: 24px;
+    letter-spacing: -0.2px;
+    margin-top: 12px;
+    margin-bottom: 12px;
+  }
+  h2 {
+    font-weight: 600;
+    line-height: normal;
+    font-size: 20px;
+    margin-top: 12px;
+    margin-bottom: 8px;
+  }
+  h3,
+  h4,
+  h5,
+  h6 {
+    font-weight: 600;
+    line-height: normal;
+    font-size: 16px;
+    letter-spacing: 0.2px;
+    margin-top: 8px;
+    margin-bottom: 4px;
+  }
+  p {
+    margin: 0 0 8px 0;
+  }
+}
+
 .dashboard .chart-header {
   position: relative;
   font-size: 16px;
@@ -44,6 +81,7 @@
     margin-left: -8px;
     height: 30px;
     width: 30px;
+    z-index: 10;
 
     &.btn.btn-primary {
       border-left-color: white;
@@ -109,14 +147,13 @@
     display: block;
   }
 
-  a[role="menuitem"] & {
+  a[role='menuitem'] & {
     width: 8px;
     height: 8px;
     margin-right: 8px;
   }
 }
 
-
 .modal img.loading {
   width: 50px;
   margin: 0;
@@ -145,11 +182,34 @@
   margin: 0 20px;
 }
 
-.dashboard .title .favstar {
-  font-size: 20px;
-  line-height: 1em;
-  position: relative;
-  top: -5px;
+.dashboard-header .dashboard-component-header {
+  display: flex;
+  flex-direction: row;
+  align-items: center;
+
+  .favstar {
+    font-size: 24px;
+    position: relative;
+    margin-left: 8px;
+  }
+
+  /* @TODO remove upon v1 deprecation */
+  .v2-preview-badge {
+    margin-left: 8px;
+    text-transform: uppercase;
+    font-size: 12px;
+    font-weight: 400;
+    background: linear-gradient(to bottom right, @pink, @purple);
+    color: white;
+    padding: 4px 8px;
+    line-height: 1em;
+    cursor: pointer;
+    opacity: 0.9;
+
+    &:hover {
+      opacity: 1;
+    }
+  }
 }
 
 .ace_gutter {
diff --git a/superset/assets/src/dashboard/stylesheets/popover-menu.less b/superset/assets/src/dashboard/stylesheets/popover-menu.less
index d69006c..0c70f58 100644
--- a/superset/assets/src/dashboard/stylesheets/popover-menu.less
+++ b/superset/assets/src/dashboard/stylesheets/popover-menu.less
@@ -48,6 +48,7 @@
 
 .dashboard-component-tabs li .popover-menu {
   top: -56px;
+  left: -7px;
 }
 
 .popover-menu .menu-item {
diff --git a/superset/assets/src/dashboard/util/getDropPosition.js b/superset/assets/src/dashboard/util/getDropPosition.js
index 2a02702..74dfcaa 100644
--- a/superset/assets/src/dashboard/util/getDropPosition.js
+++ b/superset/assets/src/dashboard/util/getDropPosition.js
@@ -9,6 +9,16 @@ export const DROP_LEFT = 'DROP_LEFT';
 // this defines how close the mouse must be to the edge of a component to display
 // a sibling type drop indicator
 const SIBLING_DROP_THRESHOLD = 20;
+const NON_SHALLOW_DROP_THRESHOLD = 20;
+
+// We cache the last recorded clientOffset per component in order to
+// have access to it beyond the handleHover phase and into the handleDrop phase
+// of drag-and-drop. we do not have access to it during drop because react-dnd's
+// monitor.getClientOffset() returns null at this point
+let CACHED_CLIENT_OFFSET = {};
+export function clearDropCache() {
+  CACHED_CLIENT_OFFSET = {};
+}
 
 export default function getDropPosition(monitor, Component) {
   const {
@@ -22,10 +32,15 @@ export default function getDropPosition(monitor, Component) {
   const draggingItem = monitor.getItem();
 
   // if dropped self on self, do nothing
+  if (!draggingItem || draggingItem.id === component.id) {
+    return null;
+  }
+
+  // TODO need a better solution to prevent nested tabs
   if (
-    !draggingItem ||
-    draggingItem.id === component.id ||
-    !isDraggingOverShallow
+    draggingItem.type === TABS_TYPE &&
+    component.type === TAB_TYPE &&
+    componentDepth === 2
   ) {
     return null;
   }
@@ -66,7 +81,37 @@ export default function getDropPosition(monitor, Component) {
   }
 
   const refBoundingRect = Component.ref.getBoundingClientRect();
-  const clientOffset = monitor.getClientOffset();
+  const clientOffset =
+    monitor.getClientOffset() || CACHED_CLIENT_OFFSET[component.id];
+
+  if (!clientOffset || !refBoundingRect) {
+    return null;
+  }
+
+  CACHED_CLIENT_OFFSET[component.id] = clientOffset;
+  const deltaTop = Math.abs(clientOffset.y - refBoundingRect.top);
+  const deltaBottom = Math.abs(clientOffset.y - refBoundingRect.bottom);
+  const deltaLeft = Math.abs(clientOffset.x - refBoundingRect.left);
+  const deltaRight = Math.abs(clientOffset.x - refBoundingRect.right);
+
+  // Most of the time we only want a drop indicator for shallow (top-level, non-nested) drop targets
+  // However there are some cases where considering only shallow targets would result in NO drop
+  // indicators which is a bad UX.
+  // e.g.,
+  //    when dragging row-a over a chart that's in another row-b, the chart is the shallow droptarget
+  //    but row-a is not a valid child or sibling. in this case we want to show a sibling drop
+  //    indicator for row-b, which is NOT a shallow drop target.
+  // BUT if we ALWAYS consider non-shallow drop targets we may get multiple indicators shown at the
+  // same time, which is also a bad UX. to prevent this we can enforce a threshold proximity of the
+  // mouse to the edge of a non-shallow target
+  if (
+    !isDraggingOverShallow &&
+    [deltaTop, deltaBottom, deltaLeft, deltaRight].every(
+      delta => delta > NON_SHALLOW_DROP_THRESHOLD,
+    )
+  ) {
+    return null;
+  }
 
   // Drop based on mouse position relative to component center
   if (validSibling && !validChild) {
@@ -83,11 +128,6 @@ export default function getDropPosition(monitor, Component) {
 
   // either is valid, so choose location based on boundary deltas
   if (validSibling && validChild) {
-    const deltaTop = Math.abs(clientOffset.y - refBoundingRect.top);
-    const deltaBottom = Math.abs(clientOffset.y - refBoundingRect.bottom);
-    const deltaLeft = Math.abs(clientOffset.x - refBoundingRect.left);
-    const deltaRight = Math.abs(clientOffset.x - refBoundingRect.right);
-
     // if near enough to a sibling boundary, drop there
     if (siblingDropOrientation === 'vertical') {
       if (deltaLeft < SIBLING_DROP_THRESHOLD) return DROP_LEFT;
diff --git a/superset/assets/src/dashboard/util/injectCustomCss.js b/superset/assets/src/dashboard/util/injectCustomCss.js
new file mode 100644
index 0000000..985d4aa
--- /dev/null
+++ b/superset/assets/src/dashboard/util/injectCustomCss.js
@@ -0,0 +1,17 @@
+export default function injectCustomCss(css) {
+  const className = 'CssEditor-css';
+  const head = document.head || document.getElementsByTagName('head')[0];
+  let style = document.querySelector(`.${className}`);
+
+  if (!style) {
+    style = document.createElement('style');
+    style.className = className;
+    style.type = 'text/css';
+    head.appendChild(style);
+  }
+  if (style.styleSheet) {
+    style.styleSheet.cssText = css;
+  } else {
+    style.innerHTML = css;
+  }
+}
diff --git a/superset/assets/src/dashboard/util/isValidChild.js b/superset/assets/src/dashboard/util/isValidChild.js
index 80bf69e..c975496 100644
--- a/superset/assets/src/dashboard/util/isValidChild.js
+++ b/superset/assets/src/dashboard/util/isValidChild.js
@@ -69,7 +69,7 @@ const parentMaxDepthLookup = {
     [DIVIDER_TYPE]: depthTwo,
     [HEADER_TYPE]: depthTwo,
     [ROW_TYPE]: depthTwo,
-    [TABS_TYPE]: rootDepth, // you cannot drop a Tabs within a Tab
+    [TABS_TYPE]: depthTwo,
   },
 
   [COLUMN_TYPE]: {
diff --git a/superset/assets/src/dashboard/util/propShapes.jsx b/superset/assets/src/dashboard/util/propShapes.jsx
index 1242d2b..3427520 100644
--- a/superset/assets/src/dashboard/util/propShapes.jsx
+++ b/superset/assets/src/dashboard/util/propShapes.jsx
@@ -35,6 +35,7 @@ export const toastShape = PropTypes.shape({
     DANGER_TOAST,
   ]).isRequired,
   text: PropTypes.string.isRequired,
+  duration: PropTypes.number,
 });
 
 export const chartPropShape = PropTypes.shape({
diff --git a/superset/assets/src/logger.js b/superset/assets/src/logger.js
index 06059b2..ea8e0fb 100644
--- a/superset/assets/src/logger.js
+++ b/superset/assets/src/logger.js
@@ -141,6 +141,13 @@ export const LOG_ACTIONS_EXPLORE_DASHBOARD_CHART = 'explore_dashboard_chart';
 export const LOG_ACTIONS_EXPORT_CSV_DASHBOARD_CHART = 'export_csv_dashboard_chart';
 export const LOG_ACTIONS_CHANGE_DASHBOARD_FILTER = 'change_dashboard_filter';
 
+// @TODO remove upon v1 deprecation
+export const LOG_ACTIONS_PREVIEW_V2 = 'preview_dashboard_v2';
+export const LOG_ACTIONS_FALLBACK_TO_V1 = 'fallback_to_dashboard_v1';
+export const LOG_ACTIONS_READ_ABOUT_V2_CHANGES = 'read_about_v2_changes';
+export const LOG_ACTIONS_DISMISS_V2_PROMPT = 'dismiss_v2_conversion_prompt';
+export const LOG_ACTIONS_SHOW_V2_INFO_PROMPT = 'show_v2_conversion_prompt';
+
 export const DASHBOARD_EVENT_NAMES = [
   LOG_ACTIONS_MOUNT_DASHBOARD,
   LOG_ACTIONS_FIRST_DASHBOARD_LOAD,
@@ -152,6 +159,12 @@ export const DASHBOARD_EVENT_NAMES = [
   LOG_ACTIONS_EXPORT_CSV_DASHBOARD_CHART,
   LOG_ACTIONS_CHANGE_DASHBOARD_FILTER,
   LOG_ACTIONS_REFRESH_DASHBOARD,
+
+  LOG_ACTIONS_PREVIEW_V2,
+  LOG_ACTIONS_FALLBACK_TO_V1,
+  LOG_ACTIONS_READ_ABOUT_V2_CHANGES,
+  LOG_ACTIONS_DISMISS_V2_PROMPT,
+  LOG_ACTIONS_SHOW_V2_INFO_PROMPT,
 ];
 
 export const EXPLORE_EVENT_NAMES = [
diff --git a/superset/assets/stylesheets/dashboard_deprecated.css b/superset/assets/stylesheets/dashboard_deprecated.css
new file mode 100644
index 0000000..57bc44c
--- /dev/null
+++ b/superset/assets/stylesheets/dashboard_deprecated.css
@@ -0,0 +1,181 @@
+/* header has mysterious extra margin */
+header.top {
+  margin-bottom: -20px;
+}
+h1.outer-container {
+  display: flex;
+  flex-direction: row;
+  align-items: center;
+}
+.v2-preview-badge {
+  margin-left: 8px;
+  text-transform: uppercase;
+  font-size: 12px;
+  font-weight: 400;
+  background: linear-gradient(to bottom right, #E32364, #2C2261);
+  color: white;
+  padding: 4px 8px;
+  line-height: 1em;
+  cursor: pointer;
+  opacity: 0.9;
+}
+
+.v2-preview-badge:hover {
+  opacity: 1;
+}
+
+.dashboard a i {
+  cursor: pointer;
+}
+.dashboard i.drag {
+  cursor: move !important;
+}
+.dashboard .slice-grid .preview-holder {
+  z-index: 1;
+  position: absolute;
+  background-color: #AAA;
+  border-color: #AAA;
+  opacity: 0.3;
+}
+div.widget .chart-controls {
+  background-clip: content-box;
+  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;
+  box-shadow: none;
+  background-color: #fff;
+  overflow: visible;
+}
+
+.slice-grid .slice_container {
+  background-color: #fff;
+}
+
+.dashboard .slice-grid .dragging,
+.dashboard .slice-grid .resizing {
+  opacity: 0.5;
+}
+.dashboard img.loading {
+  width: 20px;
+  margin: 5px;
+  position: absolute;
+}
+
+.dashboard .slice_title {
+  text-align: center;
+  font-weight: bold;
+  font-size: 14px;
+  padding: 5px;
+}
+.dashboard div.slice_content {
+  width: 100%;
+  height: 100%;
+}
+
+.modal img.loading {
+  width: 50px;
+  margin: 0;
+  position: relative;
+}
+
+.react-bs-container-body {
+  max-height: 400px;
+  overflow-y: auto;
+}
+
+.hidden, #pageDropDown {
+  display: none;
+}
+
+.slice-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%;
+}
+
+.slice-cell-highlight {
+  box-shadow: 0px 0px 20px 5px rgba(0,0,0,0.2);
+  height: 100%;
+}
+
+.slice-cell .editable-title input[type="button"] {
+  font-weight: bold;
+}
+
+.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;
+}
+
+.chart-header .header {
+  font-size: 16px;
+  margin: 0 -10px;
+}
+.ace_gutter {
+    z-index: 0;
+}
+.ace_content {
+    z-index: 0;
+}
+.ace_scrollbar {
+    z-index: 0;
+}
+.slice_container .alert {
+    margin: 10px;
+}
+
+i.danger {
+  color: red;
+}
+
+i.warning {
+  color: orange;
+}
diff --git a/superset/assets/webpack.config.js b/superset/assets/webpack.config.js
index 27bafa8..96554d6 100644
--- a/superset/assets/webpack.config.js
+++ b/superset/assets/webpack.config.js
@@ -20,6 +20,7 @@ const config = {
     addSlice: ['babel-polyfill', APP_DIR + '/src/addSlice/index.jsx'],
     explore: ['babel-polyfill', APP_DIR + '/src/explore/index.jsx'],
     dashboard: ['babel-polyfill', APP_DIR + '/src/dashboard/index.jsx'],
+    dashboard_deprecated: ['babel-polyfill', APP_DIR + '/src/dashboard/deprecated/v1/index.jsx'],
     sqllab: ['babel-polyfill', APP_DIR + '/src/SqlLab/index.jsx'],
     welcome: ['babel-polyfill', APP_DIR + '/src/welcome/index.jsx'],
     profile: ['babel-polyfill', APP_DIR + '/src/profile/index.jsx'],
diff --git a/superset/config.py b/superset/config.py
index 530b126..d03520f 100644
--- a/superset/config.py
+++ b/superset/config.py
@@ -413,6 +413,14 @@ SQL_QUERY_MUTATOR = None
 # using flask-compress
 ENABLE_FLASK_COMPRESS = True
 
+# Dashboard v1 deprecation configuration
+DASH_V2_IS_DEFAULT_VIEW_FOR_EDITORS = True
+CAN_FALLBACK_TO_DASH_V1_EDIT_MODE = True
+
+# these are incorporated into messages displayed to users
+PLANNED_V2_AUTO_CONVERT_DATE = None  # e.g. '2018-06-16'
+V2_FEEDBACK_URL = None  # e.g., 'https://goo.gl/forms/...'
+
 try:
     if CONFIG_PATH_ENV_VAR in os.environ:
         # Explicitly import config module that is not in pythonpath; useful
diff --git a/superset/connectors/sqla/models.py b/superset/connectors/sqla/models.py
index e08053c..2cb0e95 100644
--- a/superset/connectors/sqla/models.py
+++ b/superset/connectors/sqla/models.py
@@ -306,8 +306,8 @@ class SqlaTable(Model, BaseDatasource):
     @property
     def link(self):
         name = escape(self.name)
-        return Markup(
-            '<a href="{self.explore_url}">{name}</a>'.format(**locals()))
+        anchor = '<a target="_blank" href="{self.explore_url}">{name}</a>'
+        return Markup(anchor.format(**locals()))
 
     @property
     def schema_perm(self):
diff --git a/superset/templates/superset/dashboard_v1_deprecated.html b/superset/templates/superset/dashboard_v1_deprecated.html
new file mode 100644
index 0000000..1a158d9
--- /dev/null
+++ b/superset/templates/superset/dashboard_v1_deprecated.html
@@ -0,0 +1,10 @@
+{% extends "superset/basic.html" %}
+
+{% block body %}
+<div
+  id="app"
+  class="dashboard container-fluid"
+  data-bootstrap="{{ bootstrap_data }}"
+>
+</div>
+{% endblock %}
diff --git a/superset/views/core.py b/superset/views/core.py
index e9c1e00..6bbd7ae 100755
--- a/superset/views/core.py
+++ b/superset/views/core.py
@@ -1601,6 +1601,33 @@ class Superset(BaseSupersetView):
     @staticmethod
     def _set_dash_metadata(dashboard, data):
         positions = data['positions']
+        is_v2_dash = (
+            isinstance(positions, dict) and
+            positions.get('DASHBOARD_VERSION_KEY') == 'v2'
+        )
+
+        # @TODO remove upon v1 deprecation
+        if not is_v2_dash:
+            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']))
+            dashboard.position_json = json.dumps(positions, indent=4, sort_keys=True)
+            md = dashboard.params_dict
+            dashboard.css = data['css']
+            dashboard.dashboard_title = data['dashboard_title']
+
+            if 'filter_immune_slices' not in md:
+                md['filter_immune_slices'] = []
+            if 'timed_refresh_immune_slices' not in md:
+                md['timed_refresh_immune_slices'] = []
+            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', '')
+            dashboard.json_metadata = json.dumps(md, indent=4)
+            return
+
         # find slices in the position data
         slice_ids = []
         slice_id_to_name = {}
@@ -2077,12 +2104,6 @@ class Superset(BaseSupersetView):
                         'superset/request_access/?'
                         'dashboard_id={dash.id}&'.format(**locals()))
 
-        # Hack to log the dashboard_id properly, even when getting a slug
-        @log_this
-        def dashboard(**kwargs):  # noqa
-            pass
-        dashboard(dashboard_id=dash.id)
-
         dash_edit_perm = check_ownership(dash, raise_if_false=False) and \
             security_manager.can_access('can_save_dash', 'Superset')
         dash_save_perm = security_manager.can_access('can_save_dash', 'Superset')
@@ -2090,6 +2111,58 @@ class Superset(BaseSupersetView):
         slice_can_edit = security_manager.can_access('can_edit', 'SliceModelView')
 
         standalone_mode = request.args.get('standalone') == 'true'
+        edit_mode = request.args.get('edit') == 'true'
+
+        # TODO remove switch upon v1 deprecation 🎉
+        # during v2 rollout, multiple factors determine whether we show v1 or v2
+        # if layout == v1
+        #   view = v1 for non-editors
+        #   view = v1 or v2 for editors depending on config + request (force)
+        #   edit = v1 or v2 for editors depending on config + request (force)
+        #
+        # if layout == v2 (not backwards compatible)
+        #   view = v2
+        #   edit = v2
+        dashboard_layout = dash.data.get('position_json', {})
+        is_v2_dash = (
+            isinstance(dashboard_layout, dict) and
+            dashboard_layout.get('DASHBOARD_VERSION_KEY') == 'v2'
+        )
+
+        force_v1 = request.args.get('version') == 'v1' and not is_v2_dash
+        force_v2 = request.args.get('version') == 'v2'
+        force_v2_edit = (
+            is_v2_dash or
+            not app.config.get('CAN_FALLBACK_TO_DASH_V1_EDIT_MODE')
+        )
+        v2_is_default_view = app.config.get('DASH_V2_IS_DEFAULT_VIEW_FOR_EDITORS')
+        prompt_v2_conversion = False
+        if is_v2_dash:
+            dashboard_view = 'v2'
+        elif not dash_edit_perm:
+            dashboard_view = 'v1'
+        else:
+            if force_v2 or (v2_is_default_view and not force_v1):
+                dashboard_view = 'v2'
+            else:
+                dashboard_view = 'v1'
+                prompt_v2_conversion = not force_v1
+
+        # Hack to log the dashboard_id properly, even when getting a slug
+        @log_this
+        def dashboard(**kwargs):  # noqa
+            pass
+
+        # TODO remove extra logging upon v1 deprecation 🎉
+        dashboard(
+            dashboard_id=dash.id,
+            dashboard_version='v2' if is_v2_dash else 'v1',
+            dashboard_view=dashboard_view,
+            dash_edit_perm=dash_edit_perm,
+            force_v1=force_v1,
+            force_v2=force_v2,
+            force_v2_edit=force_v2_edit,
+            edit_mode=edit_mode)
 
         dashboard_data = dash.data
         dashboard_data.update({
@@ -2105,15 +2178,27 @@ class Superset(BaseSupersetView):
             'dashboard_data': dashboard_data,
             'datasources': {ds.uid: ds.data for ds in datasources},
             'common': self.common_bootsrap_payload(),
-            'editMode': request.args.get('edit') == 'true',
+            'editMode': edit_mode,
+            # TODO remove the following upon v1 deprecation 🎉
+            'force_v2_edit': force_v2_edit,
+            'prompt_v2_conversion': prompt_v2_conversion,
+            'v2_auto_convert_date': app.config.get('PLANNED_V2_AUTO_CONVERT_DATE'),
+            'v2_feedback_url': app.config.get('V2_FEEDBACK_URL'),
         }
 
         if request.args.get('json') == 'true':
             return json_success(json.dumps(bootstrap_data))
 
+        if dashboard_view == 'v2':
+            entry = 'dashboard'
+            template = 'superset/dashboard.html'
+        else:
+            entry = 'dashboard_deprecated'
+            template = 'superset/dashboard_v1_deprecated.html'
+
         return self.render_template(
-            'superset/dashboard.html',
-            entry='dashboard',
+            template,
+            entry=entry,
             standalone_mode=standalone_mode,
             title=dash.dashboard_title,
             bootstrap_data=json.dumps(bootstrap_data),
diff --git a/tests/dashboard_tests.py b/tests/dashboard_tests.py
index e9cd85b..f1b035a 100644
--- a/tests/dashboard_tests.py
+++ b/tests/dashboard_tests.py
@@ -61,7 +61,9 @@ class DashboardTests(SupersetTestCase):
         self.login(username=username)
         dash = db.session.query(models.Dashboard).filter_by(
             slug='births').first()
-        positions = {}
+        positions = {
+            'DASHBOARD_VERSION_KEY': 'v2',
+        }
         for i, slc in enumerate(dash.slices):
             id = 'DASHBOARD_CHART_TYPE-{}'.format(i)
             d = {
@@ -89,7 +91,9 @@ class DashboardTests(SupersetTestCase):
         self.login(username=username)
         dash = db.session.query(models.Dashboard).filter_by(
             slug='world_health').first()
-        positions = {}
+        positions = {
+            'DASHBOARD_VERSION_KEY': 'v2',
+        }
         for i, slc in enumerate(dash.slices):
             id = 'DASHBOARD_CHART_TYPE-{}'.format(i)
             d = {
@@ -134,7 +138,9 @@ class DashboardTests(SupersetTestCase):
             .first()
         )
         origin_title = dash.dashboard_title
-        positions = {}
+        positions = {
+            'DASHBOARD_VERSION_KEY': 'v2',
+        }
         for i, slc in enumerate(dash.slices):
             id = 'DASHBOARD_CHART_TYPE-{}'.format(i)
             d = {
@@ -170,7 +176,9 @@ class DashboardTests(SupersetTestCase):
         self.login(username=username)
         dash = db.session.query(models.Dashboard).filter_by(
             slug='births').first()
-        positions = {}
+        positions = {
+            'DASHBOARD_VERSION_KEY': 'v2',
+        }
         for i, slc in enumerate(dash.slices):
             id = 'DASHBOARD_CHART_TYPE-{}'.format(i)
             d = {
@@ -242,7 +250,9 @@ class DashboardTests(SupersetTestCase):
         self.login(username=username)
         dash = db.session.query(models.Dashboard).filter_by(
             slug='births').first()
-        positions = {}
+        positions = {
+            'DASHBOARD_VERSION_KEY': 'v2',
+        }
         origin_slices_length = len(dash.slices)
         for i, slc in enumerate(dash.slices):
             id = 'DASHBOARD_CHART_TYPE-{}'.format(i)


Mime
View raw message