superset-notifications mailing list archives

Site index · List index
Message view « Date » · « Thread »
Top « Date » · « Thread »
From GitBox <...@apache.org>
Subject [GitHub] williaster closed pull request #5126: [dashboard v2] add v1 switch
Date Wed, 06 Jun 2018 21:10:39 GMT
williaster closed pull request #5126: [dashboard v2] add v1 switch
URL: https://github.com/apache/incubator-superset/pull/5126
 
 
   

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

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

diff --git a/superset/assets/.eslintignore b/superset/assets/.eslintignore
index 7479173e66..61262fc2e1 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 e880a9cc2a..160cb31f2e 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'",
@@ -92,12 +92,14 @@
     "react-alert": "^2.3.0",
     "react-bootstrap": "^0.31.5",
     "react-bootstrap-slider": "2.0.1",
+    "react-bootstrap-table": "^4.0.2",
     "react-color": "^2.13.8",
     "react-datetime": "2.9.0",
     "react-dnd": "^2.5.4",
     "react-dnd-html5-backend": "^2.5.4",
     "react-dom": "^15.6.2",
     "react-gravatar": "^2.6.1",
+    "react-grid-layout": "0.16.5",
     "react-map-gl": "^3.0.4",
     "react-markdown": "^3.3.0",
     "react-redux": "^5.0.2",
diff --git a/superset/assets/spec/javascripts/dashboard/actions/dashboardLayout_spec.js b/superset/assets/spec/javascripts/dashboard/actions/dashboardLayout_spec.js
index 0c4fe1297c..84f0856892 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 6b5d051859..4c3185fecd 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 7e9de51c8c..3121e7e333 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 9d05344e03..fd640d1f8b 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 cbe1729a4a..dd933ac572 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 89c4ffea0f..f8095cd875 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 ec57494717..3563059d7b 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 060249fbba..1718fc78f5 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 d210ee64f3..c4908b0513 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 fde02c4102..e5c04e6ca4 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 7fd9ba4caa..c8266ad162 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 9d54b09e5a..0000000000
--- 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 62bcbb5ef1..99e93aa444 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 30e2e78776..2156ed3aa5 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 77503bb1fc..46890514f0 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 31bd08c4f7..5fa4afe167 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 0000000000..7b8a245074
--- /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 9d6333120a..f5ad9d06db 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 d8ed53ead8..9e68278852 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 3c5a3caaaf..a2b5f0a68c 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 3739b18385..faeeffa871 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 a303e133f0..cb98a6fcf0 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 ab030f4764..9ad9522965 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 459f89a771..a49a893a63 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 63619c1574..4cba2e6d7c 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 5631a25d1f..c046c02707 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 19be06cd0e..32eda1a9d1 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 0000000000..876fa78685
--- /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 0000000000..a0b7eed545
--- /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/dashboard/deprecated/chart/Chart.jsx b/superset/assets/src/dashboard/deprecated/chart/Chart.jsx
new file mode 100644
index 0000000000..bade493203
--- /dev/null
+++ b/superset/assets/src/dashboard/deprecated/chart/Chart.jsx
@@ -0,0 +1,259 @@
+/* eslint camelcase: 0 */
+import React from 'react';
+import PropTypes from 'prop-types';
+import Mustache from 'mustache';
+import { Tooltip } from 'react-bootstrap';
+
+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 './chart.css';
+
+const propTypes = {
+  annotationData: PropTypes.object,
+  actions: PropTypes.object,
+  chartKey: PropTypes.string.isRequired,
+  containerId: PropTypes.string.isRequired,
+  datasource: PropTypes.object.isRequired,
+  formData: PropTypes.object.isRequired,
+  headerHeight: PropTypes.number,
+  height: PropTypes.number,
+  width: PropTypes.number,
+  setControlValue: PropTypes.func,
+  timeout: PropTypes.number,
+  vizType: PropTypes.string.isRequired,
+  // state
+  chartAlert: PropTypes.string,
+  chartStatus: PropTypes.string,
+  chartUpdateEndTime: PropTypes.number,
+  chartUpdateStartTime: PropTypes.number,
+  latestQueryFormData: PropTypes.object,
+  queryRequest: PropTypes.object,
+  queryResponse: PropTypes.object,
+  lastRendered: PropTypes.number,
+  triggerQuery: PropTypes.bool,
+  refreshOverlayVisible: PropTypes.bool,
+  errorMessage: PropTypes.node,
+  // dashboard callbacks
+  addFilter: PropTypes.func,
+  getFilters: PropTypes.func,
+  clearFilter: PropTypes.func,
+  removeFilter: PropTypes.func,
+  onQuery: PropTypes.func,
+  onDismissRefreshOverlay: PropTypes.func,
+};
+
+const defaultProps = {
+  addFilter: () => ({}),
+  getFilters: () => ({}),
+  clearFilter: () => ({}),
+  removeFilter: () => ({}),
+};
+
+class Chart extends React.PureComponent {
+  constructor(props) {
+    super(props);
+    this.state = {};
+    // these properties are used by visualizations
+    this.annotationData = props.annotationData;
+    this.containerId = props.containerId;
+    this.selector = `#${this.containerId}`;
+    this.formData = props.formData;
+    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);
+  }
+
+  componentDidMount() {
+    if (this.props.triggerQuery) {
+      this.props.actions.runQuery(this.props.formData, false,
+        this.props.timeout,
+        this.props.chartKey,
+      );
+    }
+  }
+
+  componentWillReceiveProps(nextProps) {
+    this.annotationData = nextProps.annotationData;
+    this.containerId = nextProps.containerId;
+    this.selector = `#${this.containerId}`;
+    this.formData = nextProps.formData;
+    this.datasource = nextProps.datasource;
+  }
+
+  componentDidUpdate(prevProps) {
+    if (
+        this.props.queryResponse &&
+        ['success', 'rendered'].indexOf(this.props.chartStatus) > -1 &&
+        !this.props.queryResponse.error && (
+        prevProps.annotationData !== this.props.annotationData ||
+        prevProps.queryResponse !== this.props.queryResponse ||
+        prevProps.height !== this.props.height ||
+        prevProps.width !== this.props.width ||
+        prevProps.lastRendered !== this.props.lastRendered)
+    ) {
+      this.renderViz();
+    }
+  }
+
+  getFilters() {
+    return this.props.getFilters();
+  }
+
+  setTooltip(tooltip) {
+    this.setState({ tooltip });
+  }
+
+  addFilter(col, vals, merge = true, refresh = true) {
+    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.el.offsetWidth;
+  }
+
+  headerHeight() {
+    return this.props.headerHeight || 0;
+  }
+
+  height() {
+    return this.props.height || this.container.el.offsetHeight;
+  }
+
+  d3format(col, number) {
+    const { datasource } = this.props;
+    const format = (datasource.column_formats && datasource.column_formats[col]) || '0.3s';
+
+    return d3format(format, number);
+  }
+
+  error(e) {
+    this.props.actions.chartRenderingFailed(e, this.props.chartKey);
+  }
+
+  verboseMetricName(metric) {
+    return this.props.datasource.verbose_map[metric] || metric;
+  }
+
+  render_template(s) {
+    const context = {
+      width: this.width(),
+      height: this.height(),
+    };
+    return Mustache.render(s, context);
+  }
+
+  renderTooltip() {
+    if (this.state.tooltip) {
+      /* eslint-disable react/no-danger */
+      return (
+        <Tooltip
+          className="chart-tooltip"
+          id="chart-tooltip"
+          placement="right"
+          positionTop={this.state.tooltip.y - 10}
+          positionLeft={this.state.tooltip.x + 30}
+          arrowOffsetTop={10}
+        >
+          <div dangerouslySetInnerHTML={{ __html: this.state.tooltip.content }} />
+        </Tooltip>
+      );
+      /* eslint-enable react/no-danger */
+    }
+    return null;
+  }
+
+  renderViz() {
+    const viz = visMap[this.props.vizType];
+    const fd = this.props.formData;
+    const qr = this.props.queryResponse;
+    const renderStart = Logger.getTimestamp();
+    try {
+      // Executing user-defined data mutator function
+      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, {
+        slice_id: this.props.chartKey,
+        viz_type: this.props.vizType,
+        start_offset: renderStart,
+        duration: Logger.getTimestamp() - renderStart,
+      });
+      this.props.actions.chartRenderingSucceeded(this.props.chartKey);
+    } catch (e) {
+      this.props.actions.chartRenderingFailed(e, this.props.chartKey);
+    }
+  }
+
+  render() {
+    const isLoading = this.props.chartStatus === 'loading';
+    return (
+      <div className={`token col-md-12 ${isLoading ? 'is-loading' : ''}`}>
+        {this.renderTooltip()}
+        {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;
+            }}
+          />
+        }
+      </div>
+    );
+  }
+}
+
+Chart.propTypes = propTypes;
+Chart.defaultProps = defaultProps;
+
+export default Chart;
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 0000000000..b459f44182
--- /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 0000000000..b731412fc5
--- /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 0000000000..eda2054f92
--- /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 0000000000..52f9c47076
--- /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 0000000000..8d11249598
--- /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 0000000000..7381486f24
--- /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 0000000000..3f802c3471
--- /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 0000000000..6a6fa47bb9
--- /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 0000000000..ee11ff26d6
--- /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 0000000000..6ba4159388
--- /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 0000000000..a18a5d2990
--- /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 0000000000..d68b427d79
--- /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 0000000000..ef0ec24796
--- /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 0000000000..a84ee89b54
--- /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 0000000000..3e43f9365e
--- /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 0000000000..aa622ab078
--- /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 0000000000..6c2f62462b
--- /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 0000000000..a8c6aa7e4b
--- /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 0000000000..d7e898e901
--- /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 0000000000..00bf2bfec1
--- /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 4b3ee49722..396a56ca27 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 410ecc0776..5312de2578 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 d56e480b74..f4e091e9ac 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 5f87d0c10f..bbcb7e1ca7 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 ecf192ec1e..e93c6dbb56 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 d377c68b61..2cfd92949d 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 decb1ad093..b330f7926e 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 02039b49b1..b1124da0dd 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 57567860db..3db5cdcf15 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 d69006c788..0c70f58f64 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 2a02702088..74dfcaa0e2 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 0000000000..985d4aaff4
--- /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 80bf69ea6d..c975496baa 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 1242d2bde5..3427520644 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 06059b28b1..ea8e0fbf9d 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 0000000000..57bc44cc54
--- /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 6fd88d7185..d26f9d854a 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 530b126896..d03520fe90 100644
--- a/superset/config.py
+++ b/superset/config.py
@@ -413,6 +413,14 @@ class CeleryConfig(object):
 # 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 875707f55c..513ebf943c 100644
--- a/superset/connectors/sqla/models.py
+++ b/superset/connectors/sqla/models.py
@@ -306,8 +306,8 @@ def description_markeddown(self):
     @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 0000000000..1a158d92a7
--- /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 2d2c5fc059..f33ceeba3b 100755
--- a/superset/views/core.py
+++ b/superset/views/core.py
@@ -1604,6 +1604,33 @@ def save_dash(self, dashboard_id):
     @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 = {}
@@ -2080,12 +2107,6 @@ def dashboard(self, dashboard_id):
                         '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')
@@ -2093,6 +2114,58 @@ def dashboard(**kwargs):  # noqa
         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({
@@ -2108,15 +2181,27 @@ def dashboard(**kwargs):  # noqa
             '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 6b6d656cff..b82b9af0e7 100644
--- a/tests/dashboard_tests.py
+++ b/tests/dashboard_tests.py
@@ -61,7 +61,9 @@ def test_save_dash(self, username='admin'):
         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 @@ def test_save_dash_with_filter(self, username='admin'):
         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 @@ def test_save_dash_with_dashboard_title(self, username='admin'):
             .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 @@ def test_copy_dash(self, username='admin'):
         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 @@ def test_remove_slices(self, username='admin'):
         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)


 

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


With regards,
Apache Git Services

---------------------------------------------------------------------
To unsubscribe, e-mail: notifications-unsubscribe@superset.apache.org
For additional commands, e-mail: notifications-help@superset.apache.org


Mime
View raw message