superset-commits mailing list archives

Site index · List index
Message view « Date » · « Thread »
Top « Date » · « Thread »
From grace...@apache.org
Subject [incubator-superset] branch master updated: Dashboard refactory (#3581)
Date Wed, 08 Nov 2017 18:46:23 GMT
This is an automated email from the ASF dual-hosted git repository.

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


The following commit(s) were added to refs/heads/master by this push:
     new 4fa1f0a  Dashboard refactory (#3581)
4fa1f0a is described below

commit 4fa1f0ab17e239e95826a020653f7bd21df46493
Author: Grace Guo <grace.guo@airbnb.com>
AuthorDate: Wed Nov 8 10:46:21 2017 -0800

    Dashboard refactory (#3581)
    
    Create Chart component for all chart fetching and rendering, and apply redux architecture in dashboard view.
---
 superset/assets/javascripts/chart/Chart.jsx        | 184 +++++++++++
 superset/assets/javascripts/chart/ChartBody.jsx    |  54 +++
 .../assets/javascripts/chart/ChartContainer.jsx    |  28 ++
 superset/assets/javascripts/chart/chartAction.js   |  91 ++++++
 superset/assets/javascripts/chart/chartReducer.js  | 100 ++++++
 .../assets/javascripts/components/FaveStar.jsx     |   9 +-
 .../javascripts/components/StackTraceMessage.jsx   |  59 ++++
 .../assets/javascripts/dashboard/Dashboard.jsx     |   4 +-
 superset/assets/javascripts/dashboard/actions.js   | 112 +++++++
 .../javascripts/dashboard/components/Controls.jsx  |  26 +-
 .../javascripts/dashboard/components/Dashboard.jsx | 349 ++++++++++++++++++++
 .../dashboard/components/DashboardAlert.jsx        |  21 ++
 .../dashboard/components/DashboardContainer.jsx    |  29 ++
 .../javascripts/dashboard/components/GridCell.jsx  | 132 ++++++++
 .../dashboard/components/GridLayout.jsx            | 218 +++++++------
 .../javascripts/dashboard/components/Header.jsx    |  44 ++-
 .../javascripts/dashboard/components/SaveModal.jsx |  21 +-
 .../dashboard/components/SliceAdder.jsx            |  26 +-
 .../javascripts/dashboard/components/SliceCell.jsx | 117 -------
 .../dashboard/components/SliceHeader.jsx           | 142 ++++++++
 superset/assets/javascripts/dashboard/index.jsx    |  29 ++
 superset/assets/javascripts/dashboard/reducers.js  | 188 +++++++++++
 .../javascripts/explore/actions/chartActions.js    |  74 -----
 .../javascripts/explore/actions/exploreActions.js  |  11 +-
 .../explore/components/ChartContainer.jsx          | 362 ---------------------
 .../explore/components/EmbedCodeButton.jsx         |   4 +-
 .../explore/components/ExploreChartHeader.jsx      | 140 ++++++++
 .../explore/components/ExploreChartPanel.jsx       |  79 +++++
 .../explore/components/ExploreViewContainer.jsx    |  47 +--
 .../javascripts/explore/components/SaveModal.jsx   |   6 +-
 superset/assets/javascripts/explore/index.jsx      |  22 +-
 .../javascripts/explore/reducers/chartReducer.js   |  80 -----
 .../javascripts/explore/reducers/exploreReducer.js |   8 -
 .../assets/javascripts/explore/reducers/index.js   |   4 +-
 superset/assets/javascripts/modules/utils.js       |   8 +
 superset/assets/javascripts/reduxUtils.js          |  19 +-
 .../spec/javascripts/dashboard/SliceCell_spec.jsx  |  24 --
 .../assets/spec/javascripts/dashboard/fixtures.jsx |   2 +-
 .../spec/javascripts/explore/chartActions_spec.js  |   2 +-
 ...Container_spec.js => ExploreChartPanel_spec.js} |   0
 .../explore/components/SaveModal_spec.jsx          |   2 +-
 .../javascripts/explore/exploreActions_spec.js     |   3 +-
 superset/assets/stylesheets/dashboard.css          |  19 +-
 superset/assets/stylesheets/superset.less          |  19 +-
 superset/assets/visualizations/markup.js           |   2 +-
 superset/assets/webpack.config.js                  |   2 +-
 superset/templates/superset/dashboard.html         |   6 -
 47 files changed, 2050 insertions(+), 878 deletions(-)

diff --git a/superset/assets/javascripts/chart/Chart.jsx b/superset/assets/javascripts/chart/Chart.jsx
new file mode 100644
index 0000000..f775e89
--- /dev/null
+++ b/superset/assets/javascripts/chart/Chart.jsx
@@ -0,0 +1,184 @@
+/* eslint camelcase: 0 */
+import React from 'react';
+import PropTypes from 'prop-types';
+import Mustache from 'mustache';
+
+import { d3format } from '../modules/utils';
+import ChartBody from './ChartBody';
+import Loading from '../components/Loading';
+import StackTraceMessage from '../components/StackTraceMessage';
+import visMap from '../../visualizations/main';
+
+const propTypes = {
+  actions: PropTypes.object,
+  chartKey: PropTypes.string.isRequired,
+  containerId: PropTypes.string.isRequired,
+  datasource: PropTypes.object.isRequired,
+  formData: PropTypes.object.isRequired,
+  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,
+  // dashboard callbacks
+  addFilter: PropTypes.func,
+  getFilters: PropTypes.func,
+  clearFilter: PropTypes.func,
+  removeFilter: PropTypes.func,
+};
+
+const defaultProps = {
+  addFilter: () => ({}),
+  getFilters: () => ({}),
+  clearFilter: () => ({}),
+  removeFilter: () => ({}),
+};
+
+class Chart extends React.PureComponent {
+  constructor(props) {
+    super(props);
+
+    // these properties are used by visualizations
+    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.height = this.height.bind(this);
+    this.width = this.width.bind(this);
+  }
+
+  componentDidMount() {
+    this.runQuery();
+  }
+
+  componentWillReceiveProps(nextProps) {
+    this.containerId = nextProps.containerId;
+    this.selector = `#${this.containerId}`;
+    this.formData = nextProps.formData;
+    this.datasource = nextProps.datasource;
+  }
+
+  componentDidUpdate(prevProps) {
+    if (
+        this.props.queryResponse &&
+        this.props.chartStatus === 'success' &&
+        !this.props.queryResponse.error && (
+        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();
+  }
+
+  addFilter(col, vals, merge = true, refresh = true) {
+    this.props.addFilter(col, vals, merge, refresh);
+  }
+
+  clearFilter() {
+    this.props.clearFilter();
+  }
+
+  removeFilter(col, vals) {
+    this.props.removeFilter(col, vals);
+  }
+
+  clearError() {
+    this.setState({
+      errorMsg: null,
+    });
+  }
+
+  width() {
+    return this.props.width || this.container.el.offsetWidth;
+  }
+
+  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);
+  }
+
+  runQuery() {
+    this.props.actions.runQuery(this.props.formData, true,
+      this.props.timeout,
+      this.props.chartKey,
+    );
+  }
+
+  render_template(s) {
+    const context = {
+      width: this.width(),
+      height: this.height(),
+    };
+    return Mustache.render(s, context);
+  }
+
+  renderViz() {
+    const viz = visMap[this.props.vizType];
+    try {
+      viz(this, this.props.queryResponse, this.props.actions.setControlValue);
+    } 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' : ''}`}>
+        {isLoading &&
+          <Loading size={25} />
+        }
+
+        {this.props.chartAlert &&
+        <StackTraceMessage
+          message={this.props.chartAlert}
+          queryResponse={this.props.queryResponse}
+        />
+        }
+
+        {!this.props.chartAlert &&
+          <ChartBody
+            containerId={this.containerId}
+            vizType={this.props.formData.viz_type}
+            height={this.height}
+            width={this.width}
+            ref={(inner) => {
+              this.container = inner;
+            }}
+          />
+        }
+      </div>
+    );
+  }
+}
+
+Chart.propTypes = propTypes;
+Chart.defaultProps = defaultProps;
+
+export default Chart;
diff --git a/superset/assets/javascripts/chart/ChartBody.jsx b/superset/assets/javascripts/chart/ChartBody.jsx
new file mode 100644
index 0000000..89352f5
--- /dev/null
+++ b/superset/assets/javascripts/chart/ChartBody.jsx
@@ -0,0 +1,54 @@
+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,
+};
+
+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}`}
+        ref={(el) => { this.el = el; }}
+      />
+    );
+  }
+}
+
+ChartBody.propTypes = propTypes;
+
+export default ChartBody;
diff --git a/superset/assets/javascripts/chart/ChartContainer.jsx b/superset/assets/javascripts/chart/ChartContainer.jsx
new file mode 100644
index 0000000..11c4322
--- /dev/null
+++ b/superset/assets/javascripts/chart/ChartContainer.jsx
@@ -0,0 +1,28 @@
+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 {
+    chartAlert: chart.chartAlert,
+    chartStatus: chart.chartStatus,
+    chartUpdateEndTime: chart.chartUpdateEndTime,
+    chartUpdateStartTime: chart.chartUpdateStartTime,
+    latestQueryFormData: chart.latestQueryFormData,
+    queryResponse: chart.queryResponse,
+    queryRequest: chart.queryRequest,
+    triggerQuery: chart.triggerQuery,
+    triggerRender: chart.triggerRender,
+  };
+}
+
+function mapDispatchToProps(dispatch) {
+  return {
+    actions: bindActionCreators(Actions, dispatch),
+  };
+}
+
+export default connect(mapStateToProps, mapDispatchToProps)(Chart);
diff --git a/superset/assets/javascripts/chart/chartAction.js b/superset/assets/javascripts/chart/chartAction.js
new file mode 100644
index 0000000..17205a4
--- /dev/null
+++ b/superset/assets/javascripts/chart/chartAction.js
@@ -0,0 +1,91 @@
+import { getExploreUrl } from '../explore/exploreUtils';
+import { t } from '../locales';
+
+const $ = window.$ = require('jquery');
+
+export const CHART_UPDATE_STARTED = 'CHART_UPDATE_STARTED';
+export function chartUpdateStarted(queryRequest, key) {
+  return { type: CHART_UPDATE_STARTED, queryRequest, 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(queryRequest, key) {
+  if (queryRequest) {
+    queryRequest.abort();
+  }
+  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 REMOVE_CHART = 'REMOVE_CHART';
+export function removeChart(key) {
+  return { type: REMOVE_CHART, key };
+}
+
+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 RUN_QUERY = 'RUN_QUERY';
+export function runQuery(formData, force = false, timeout = 60, key) {
+  return (dispatch) => {
+    const url = getExploreUrl(formData, 'json', force);
+    const queryRequest = $.ajax({
+      url,
+      dataType: 'json',
+      timeout: timeout * 1000,
+      success: (queryResponse =>
+        dispatch(chartUpdateSucceeded(queryResponse, key))
+      ),
+      error: ((xhr) => {
+        if (xhr.statusText === 'timeout') {
+          dispatch(chartUpdateTimeout(xhr.statusText, timeout, key));
+        } else {
+          let error = '';
+          if (!xhr.responseText) {
+            const status = xhr.status;
+            if (status === 0) {
+              // This may happen when the worker in gunicorn times out
+              error += (
+                t('The server could not be reached. You may want to ' +
+                  'verify your connection and try again.'));
+            } else {
+              error += (t('An unknown error occurred. (Status: %s )', status));
+            }
+          }
+          const errorResponse = Object.assign({}, xhr.responseJSON, error);
+          dispatch(chartUpdateFailed(errorResponse, key));
+        }
+      }),
+    });
+
+    dispatch(chartUpdateStarted(queryRequest, key));
+    dispatch(triggerQuery(false, key));
+  };
+}
diff --git a/superset/assets/javascripts/chart/chartReducer.js b/superset/assets/javascripts/chart/chartReducer.js
new file mode 100644
index 0000000..2adb904
--- /dev/null
+++ b/superset/assets/javascripts/chart/chartReducer.js
@@ -0,0 +1,100 @@
+/* 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,
+  queryResponse: PropTypes.object,
+  triggerQuery: PropTypes.bool,
+  lastRendered: PropTypes.number,
+};
+
+export const chart = {
+  chartKey: '',
+  chartAlert: null,
+  chartStatus: null,
+  chartUpdateEndTime: null,
+  chartUpdateStartTime: now(),
+  latestQueryFormData: 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',
+        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_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: (
+        "<strong>{t('Query timeout')}</strong> - " +
+        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 };
+    },
+  };
+
+  /* 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/javascripts/components/FaveStar.jsx b/superset/assets/javascripts/components/FaveStar.jsx
index e633247..60de9d1 100644
--- a/superset/assets/javascripts/components/FaveStar.jsx
+++ b/superset/assets/javascripts/components/FaveStar.jsx
@@ -5,19 +5,20 @@ import TooltipWrapper from './TooltipWrapper';
 import { t } from '../locales';
 
 const propTypes = {
-  sliceId: PropTypes.number.isRequired,
-  actions: PropTypes.object.isRequired,
+  itemId: PropTypes.number.isRequired,
+  fetchFaveStar: PropTypes.func,
+  saveFaveStar: PropTypes.func,
   isStarred: PropTypes.bool.isRequired,
 };
 
 export default class FaveStar extends React.Component {
   componentDidMount() {
-    this.props.actions.fetchFaveStar(this.props.sliceId);
+    this.props.fetchFaveStar(this.props.itemId);
   }
 
   onClick(e) {
     e.preventDefault();
-    this.props.actions.saveFaveStar(this.props.sliceId, this.props.isStarred);
+    this.props.saveFaveStar(this.props.itemId, this.props.isStarred);
   }
 
   render() {
diff --git a/superset/assets/javascripts/components/StackTraceMessage.jsx b/superset/assets/javascripts/components/StackTraceMessage.jsx
new file mode 100644
index 0000000..a950c39
--- /dev/null
+++ b/superset/assets/javascripts/components/StackTraceMessage.jsx
@@ -0,0 +1,59 @@
+/* eslint-disable react/no-danger */
+import React from 'react';
+import PropTypes from 'prop-types';
+import { Alert, Collapse } from 'react-bootstrap';
+
+const propTypes = {
+  message: PropTypes.string,
+  queryResponse: PropTypes.object,
+  showStackTrace: PropTypes.bool,
+};
+const defaultProps = {
+  showStackTrace: false,
+};
+
+class StackTraceMessage extends React.PureComponent {
+  constructor(props) {
+    super(props);
+
+    this.state = {
+      showStackTrace: props.showStackTrace,
+    };
+  }
+
+  hasTrace() {
+    return this.props.queryResponse && this.props.queryResponse.stacktrace;
+  }
+
+  render() {
+    const msg = (
+      <div>
+        <p
+          dangerouslySetInnerHTML={{ __html: this.props.message }}
+        />
+      </div>);
+
+    return (
+      <div className={`stack-trace-container${this.hasTrace() ? ' has-trace' : ''}`}>
+        <Alert
+          bsStyle="warning"
+          onClick={() => this.setState({ showStackTrace: !this.state.showStackTrace })}
+        >
+          {msg}
+        </Alert>
+        {this.hasTrace() &&
+          <Collapse in={this.state.showStackTrace}>
+            <pre>
+              {this.props.queryResponse.stacktrace}
+            </pre>
+          </Collapse>
+        }
+      </div>
+    );
+  }
+}
+
+StackTraceMessage.propTypes = propTypes;
+StackTraceMessage.defaultProps = defaultProps;
+
+export default StackTraceMessage;
diff --git a/superset/assets/javascripts/dashboard/Dashboard.jsx b/superset/assets/javascripts/dashboard/Dashboard.jsx
index f133424..9e67647 100644
--- a/superset/assets/javascripts/dashboard/Dashboard.jsx
+++ b/superset/assets/javascripts/dashboard/Dashboard.jsx
@@ -283,7 +283,7 @@ export function dashboardContainer(dashboard, datasources, userid) {
       const refreshAll = () => {
         const slices = dash.sliceObjects
           .filter(slice => immune.indexOf(slice.data.slice_id) === -1);
-        dash.renderSlices(slices, true, interval * 0.2);
+        dash.fetchSlices(slices, true, interval * 0.2);
       };
       const fetchAndRender = function () {
         refreshAll();
@@ -375,7 +375,7 @@ $(document).ready(() => {
 
   const state = getInitialState(dashboardData);
   px = superset(state);
-  const dashboard = dashboardContainer(state.dashboard, state.datasources, state.user_id);
+  const dashboard = dashboardContainer(state.dashboard, state.datasources, state.userId);
   initDashboardView(dashboard);
   dashboard.init();
 });
diff --git a/superset/assets/javascripts/dashboard/actions.js b/superset/assets/javascripts/dashboard/actions.js
new file mode 100644
index 0000000..6e88ca6
--- /dev/null
+++ b/superset/assets/javascripts/dashboard/actions.js
@@ -0,0 +1,112 @@
+/* global notify */
+import $ from 'jquery';
+import { getExploreUrl } 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) {
+  return { type: REMOVE_FILTER, sliceId, col, vals };
+}
+
+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 saveUrl = getExploreUrl(slice.form_data, 'base', false, null, sliceParams);
+    return $.ajax({
+      url: saveUrl,
+      type: 'GET',
+      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 };
+}
diff --git a/superset/assets/javascripts/dashboard/components/Controls.jsx b/superset/assets/javascripts/dashboard/components/Controls.jsx
index 5d24055..ecbc907 100644
--- a/superset/assets/javascripts/dashboard/components/Controls.jsx
+++ b/superset/assets/javascripts/dashboard/components/Controls.jsx
@@ -14,6 +14,15 @@ const $ = window.$ = require('jquery');
 
 const propTypes = {
   dashboard: PropTypes.object.isRequired,
+  slices: PropTypes.array,
+  userId: PropTypes.string.isRequired,
+  addSlicesToDashboard: PropTypes.func,
+  onSave: PropTypes.func,
+  onChange: PropTypes.func,
+  readFilters: PropTypes.func,
+  renderSlices: PropTypes.func,
+  serialize: PropTypes.func,
+  startPeriodicRender: PropTypes.func,
 };
 
 class Controls extends React.PureComponent {
@@ -36,14 +45,16 @@ class Controls extends React.PureComponent {
   }
   refresh() {
     // Force refresh all slices
-    this.props.dashboard.renderSlices(this.props.dashboard.sliceObjects, true);
+    this.props.renderSlices(true);
   }
   changeCss(css) {
     this.setState({ css });
-    this.props.dashboard.onChange();
+    this.props.onChange();
   }
   render() {
-    const dashboard = this.props.dashboard;
+    const { dashboard, userId,
+      addSlicesToDashboard, startPeriodicRender, readFilters,
+      serialize, onSave } = this.props;
     const emailBody = t('Checkout this dashboard: %s', window.location.href);
     const emailLink = 'mailto:?Subject=Superset%20Dashboard%20'
       + `${dashboard.dashboard_title}&Body=${emailBody}`;
@@ -57,18 +68,20 @@ class Controls extends React.PureComponent {
         </Button>
         <SliceAdder
           dashboard={dashboard}
+          addSlicesToDashboard={addSlicesToDashboard}
+          userId={userId}
           triggerNode={
             <i className="fa fa-plus" />
           }
         />
         <RefreshIntervalModal
-          onChange={refreshInterval => dashboard.startPeriodicRender(refreshInterval * 1000)}
+          onChange={refreshInterval => startPeriodicRender(refreshInterval * 1000)}
           triggerNode={
             <i className="fa fa-clock-o" />
           }
         />
         <CodeModal
-          codeCallback={dashboard.readFilters.bind(dashboard)}
+          codeCallback={readFilters}
           triggerNode={<i className="fa fa-filter" />}
         />
         <CssEditor
@@ -96,6 +109,9 @@ class Controls extends React.PureComponent {
         </Button>
         <SaveModal
           dashboard={dashboard}
+          readFilters={readFilters}
+          serialize={serialize}
+          onSave={onSave}
           css={this.state.css}
           triggerNode={
             <Button disabled={!dashboard.dash_save_perm}>
diff --git a/superset/assets/javascripts/dashboard/components/Dashboard.jsx b/superset/assets/javascripts/dashboard/components/Dashboard.jsx
new file mode 100644
index 0000000..2415e36
--- /dev/null
+++ b/superset/assets/javascripts/dashboard/components/Dashboard.jsx
@@ -0,0 +1,349 @@
+import React from 'react';
+import PropTypes from 'prop-types';
+
+import AlertsWrapper from '../../components/AlertsWrapper';
+import GridLayout from './GridLayout';
+import Header from './Header';
+import DashboardAlert from './DashboardAlert';
+import { getExploreUrl } from '../../explore/exploreUtils';
+import { areObjectsEqual } from '../../reduxUtils';
+import { t } from '../../locales';
+
+import '../../../stylesheets/dashboard.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,
+};
+
+const defaultProps = {
+  initMessages: [],
+  dashboard: {},
+  slices: {},
+  datasources: {},
+  filters: {},
+  timeout: 60,
+  userId: '',
+  isStarred: false,
+};
+
+class Dashboard extends React.PureComponent {
+  constructor(props) {
+    super(props);
+    this.refreshTimer = null;
+    this.firstLoad = true;
+
+    // alert for unsaved changes
+    this.state = {
+      alert: null,
+      trigger: false,
+    };
+
+    this.rerenderCharts = this.rerenderCharts.bind(this);
+    this.updateDashboardTitle = this.updateDashboardTitle.bind(this);
+    this.onSave = this.onSave.bind(this);
+    this.onChange = this.onChange.bind(this);
+    this.serialize = this.serialize.bind(this);
+    this.readFilters = this.readFilters.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.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() {
+    this.loadPreSelectFilters();
+    this.firstLoad = false;
+    window.addEventListener('resize', this.rerenderCharts);
+  }
+
+  componentWillReceiveProps(nextProps) {
+    // check filters is changed
+    if (!areObjectsEqual(nextProps.filters, this.props.filters)) {
+      this.renderUnsavedChangeAlert();
+    }
+  }
+
+  componentDidUpdate(prevProps) {
+    if (!areObjectsEqual(prevProps.filters, this.props.filters) && this.props.refresh) {
+      Object.keys(this.props.filters).forEach(sliceId => (this.refreshExcept(sliceId)));
+    }
+  }
+
+  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.renderUnsavedChangeAlert();
+  }
+
+  onSave() {
+    this.onBeforeUnload(false);
+    this.setState({
+      alert: '',
+    });
+  }
+
+  // return charts in array
+  getAllSlices() {
+    return Object.values(this.props.slices);
+  }
+
+  getFormDataExtra(slice) {
+    const formDataExtra = Object.assign({}, slice.formData);
+    const extraFilters = this.effectiveExtraFilters(slice.slice_id);
+    formDataExtra.filters = formDataExtra.filters.concat(extraFilters);
+    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;
+  }
+
+  jsonEndpoint(data, force = false) {
+    let endpoint = getExploreUrl(data, 'json', force);
+    if (endpoint.charAt(0) !== '/') {
+      // Known issue for IE <= 11:
+      // https://connect.microsoft.com/IE/feedbackdetail/view/1002846/pathname-incorrect-for-out-of-document-elements
+      endpoint = '/' + endpoint;
+    }
+    return endpoint;
+  }
+
+  loadPreSelectFilters() {
+    for (const key in this.props.filters) {
+      for (const col in this.props.filters[key]) {
+        const sliceId = parseInt(key, 10);
+        this.props.actions.addFilter(sliceId, col,
+          this.props.filters[key][col], false, false,
+        );
+      }
+    }
+  }
+
+  refreshExcept(sliceId) {
+    const immune = this.props.dashboard.metadata.filter_immune_slices || [];
+    const slices = this.getAllSlices()
+      .filter(slice => slice.slice_id !== sliceId && 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();
+  }
+
+  readFilters() {
+    // Returns a list of human readable active filters
+    return JSON.stringify(this.props.filters, null, '  ');
+  }
+
+  updateDashboardTitle(title) {
+    this.props.actions.updateDashboardTitle(title);
+    this.onChange();
+  }
+
+  serialize() {
+    return this.props.dashboard.layout.map(reactPos => ({
+      slice_id: reactPos.i,
+      col: reactPos.x + 1,
+      row: reactPos.y,
+      size_x: reactPos.w,
+      size_y: reactPos.h,
+    }));
+  }
+
+  addSlicesToDashboard(sliceIds) {
+    return this.props.actions.addSlicesToDashboard(this.props.dashboard.id, sliceIds);
+  }
+
+  fetchSlice(slice, force = false) {
+    return this.props.actions.runQuery(
+      this.getFormDataExtra(slice), force, this.props.timeout, slice.chartKey,
+    );
+  }
+
+  // fetch and render an list of slices
+  fetchSlices(slc, force = false, interval = 0) {
+    const slices = slc || this.getAllSlices();
+    if (!interval) {
+      slices.forEach((slice) => { this.fetchSlice(slice, force); });
+      return;
+    }
+
+    const meta = this.props.dashboard.metadata;
+    const refreshTime = Math.max(interval, meta.stagger_time || 5000); // default 5 seconds
+    if (typeof meta.stagger_refresh !== 'boolean') {
+      meta.stagger_refresh = meta.stagger_refresh === undefined ?
+        true : meta.stagger_refresh === 'true';
+    }
+    const delay = meta.stagger_refresh ? refreshTime / (slices.length - 1) : 0;
+    slices.forEach((slice, i) => {
+      setTimeout(() => { this.fetchSlice(slice, force); }, delay * i);
+    });
+  }
+
+  // re-render chart without fetch
+  rerenderCharts() {
+    this.getAllSlices().forEach((slice) => {
+      setTimeout(() => {
+        this.props.actions.renderTriggered(new Date().getTime(), slice.chartKey);
+      }, 50);
+    });
+  }
+
+  renderUnsavedChangeAlert() {
+    this.setState({
+      alert: (
+        <span>
+          <strong>{t('You have unsaved changes.')}</strong> {t('Click the')} &nbsp;
+          <i className="fa fa-save" />&nbsp;
+          {t('button on the top right to save your changes.')}
+        </span>
+      ),
+    });
+  }
+
+  render() {
+    return (
+      <div id="dashboard-container">
+        {this.state.alert && <DashboardAlert alertContent={this.state.alert} />}
+        <div id="dashboard-header">
+          <AlertsWrapper initMessages={this.props.initMessages} />
+          <Header
+            dashboard={this.props.dashboard}
+            userId={this.props.userId}
+            isStarred={this.props.isStarred}
+            updateDashboardTitle={this.updateDashboardTitle}
+            onSave={this.onSave}
+            onChange={this.onChange}
+            serialize={this.serialize}
+            readFilters={this.readFilters}
+            fetchFaveStar={this.props.actions.fetchFaveStar}
+            saveFaveStar={this.props.actions.saveFaveStar}
+            renderSlices={this.fetchAllSlices}
+            startPeriodicRender={this.startPeriodicRender}
+            addSlicesToDashboard={this.addSlicesToDashboard}
+          />
+        </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}
+            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}
+          />
+        </div>
+      </div>
+    );
+  }
+}
+
+Dashboard.propTypes = propTypes;
+Dashboard.defaultProps = defaultProps;
+
+export default Dashboard;
diff --git a/superset/assets/javascripts/dashboard/components/DashboardAlert.jsx b/superset/assets/javascripts/dashboard/components/DashboardAlert.jsx
new file mode 100644
index 0000000..4579ce8
--- /dev/null
+++ b/superset/assets/javascripts/dashboard/components/DashboardAlert.jsx
@@ -0,0 +1,21 @@
+import React from 'react';
+import PropTypes from 'prop-types';
+import { Alert } from 'react-bootstrap';
+
+const propTypes = {
+  alertContent: PropTypes.node.isRequired,
+};
+
+const DashboardAlert = ({ alertContent }) => (
+  <div id="alert-container">
+    <div className="container-fluid">
+      <Alert bsStyle="warning">
+        {alertContent}
+      </Alert>
+    </div>
+  </div>
+);
+
+DashboardAlert.propTypes = propTypes;
+
+export default DashboardAlert;
diff --git a/superset/assets/javascripts/dashboard/components/DashboardContainer.jsx b/superset/assets/javascripts/dashboard/components/DashboardContainer.jsx
new file mode 100644
index 0000000..24127aa
--- /dev/null
+++ b/superset/assets/javascripts/dashboard/components/DashboardContainer.jsx
@@ -0,0 +1,29 @@
+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 }) {
+  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,
+  };
+}
+
+function mapDispatchToProps(dispatch) {
+  const actions = { ...chartActions, ...dashboardActions };
+  return {
+    actions: bindActionCreators(actions, dispatch),
+  };
+}
+
+export default connect(mapStateToProps, mapDispatchToProps)(Dashboard);
diff --git a/superset/assets/javascripts/dashboard/components/GridCell.jsx b/superset/assets/javascripts/dashboard/components/GridCell.jsx
new file mode 100644
index 0000000..1a59a92
--- /dev/null
+++ b/superset/assets/javascripts/dashboard/components/GridCell.jsx
@@ -0,0 +1,132 @@
+/* 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.css';
+
+const propTypes = {
+  timeout: PropTypes.number,
+  datasource: PropTypes.object,
+  isLoading: PropTypes.bool,
+  isExpanded: PropTypes.bool,
+  widgetHeight: PropTypes.number,
+  widgetWidth: PropTypes.number,
+  exploreChartUrl: PropTypes.string,
+  exportCSVUrl: PropTypes.string,
+  slice: PropTypes.object,
+  chartKey: PropTypes.string,
+  formData: PropTypes.object,
+  filters: PropTypes.object,
+  forceRefresh: PropTypes.func,
+  removeSlice: PropTypes.func,
+  updateSliceName: PropTypes.func,
+  toggleExpandSlice: PropTypes.func,
+  addFilter: PropTypes.func,
+  getFilters: PropTypes.func,
+  clearFilter: PropTypes.func,
+  removeFilter: PropTypes.func,
+};
+
+const defaultProps = {
+  forceRefresh: () => ({}),
+  removeSlice: () => ({}),
+  updateSliceName: () => ({}),
+  toggleExpandSlice: () => ({}),
+  addFilter: () => ({}),
+  getFilters: () => ({}),
+  clearFilter: () => ({}),
+  removeFilter: () => ({}),
+};
+
+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 headerId = this.getHeaderId(slice);
+    const descriptionId = this.getDescriptionId(slice);
+    const headerHeight = this.refs[headerId] ? this.refs[headerId].offsetHeight : 30;
+    let descriptionHeight = 0;
+    if (this.props.isExpanded && this.refs[descriptionId]) {
+      descriptionHeight = this.refs[descriptionId].offsetHeight + 10;
+    }
+    return widgetHeight - headerHeight - descriptionHeight;
+  }
+
+  render() {
+    const {
+      exploreChartUrl, exportCSVUrl, isExpanded, isLoading, removeSlice, updateSliceName,
+      toggleExpandSlice, forceRefresh, chartKey, slice, datasource, formData, timeout,
+    } = this.props;
+    return (
+      <div
+        className={isLoading ? 'slice-cell-highlight' : 'slice-cell'}
+        id={`${slice.slice_id}-cell`}
+      >
+        <div ref={this.getHeaderId(slice)}>
+          <SliceHeader
+            slice={slice}
+            exploreChartUrl={exploreChartUrl}
+            exportCSVUrl={exportCSVUrl}
+            isExpanded={isExpanded}
+            removeSlice={removeSlice}
+            updateSliceName={updateSliceName}
+            toggleExpandSlice={toggleExpandSlice}
+            forceRefresh={forceRefresh}
+          />
+        </div>
+        <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}
+            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/javascripts/dashboard/components/GridLayout.jsx b/superset/assets/javascripts/dashboard/components/GridLayout.jsx
index dc99503..22d4b59 100644
--- a/superset/assets/javascripts/dashboard/components/GridLayout.jsx
+++ b/superset/assets/javascripts/dashboard/components/GridLayout.jsx
@@ -1,10 +1,8 @@
-/* global notify */
 import React from 'react';
 import PropTypes from 'prop-types';
 import { Responsive, WidthProvider } from 'react-grid-layout';
-import $ from 'jquery';
 
-import SliceCell from './SliceCell';
+import GridCell from './GridCell';
 import { getExploreUrl } from '../../explore/exploreUtils';
 
 require('react-grid-layout/css/styles.css');
@@ -14,119 +12,127 @@ 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,
+  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,
+};
+
+const defaultProps = {
+  onChange: () => ({}),
+  getFormDataExtra: () => ({}),
+  fetchSlice: () => ({}),
+  saveSlice: () => ({}),
+  removeSlice: () => ({}),
+  removeChart: () => ({}),
+  updateDashboardLayout: () => ({}),
+  toggleExpandSlice: () => ({}),
+  addFilter: () => ({}),
+  getFilters: () => ({}),
+  clearFilter: () => ({}),
+  removeFilter: () => ({}),
 };
 
 class GridLayout extends React.Component {
-  componentWillMount() {
-    const layout = [];
-
-    this.props.dashboard.slices.forEach((slice, index) => {
-      const sliceId = slice.slice_id;
-      let pos = this.props.dashboard.posDict[sliceId];
-      if (!pos) {
-        pos = {
-          col: (index * 4 + 1) % 12,
-          row: Math.floor((index) / 3) * 4,
-          size_x: 4,
-          size_y: 4,
-        };
-      }
-
-      layout.push({
-        i: String(sliceId),
-        x: pos.col - 1,
-        y: pos.row,
-        w: pos.size_x,
-        minW: 2,
-        h: pos.size_y,
-      });
-    });
-
-    this.setState({
-      layout,
-      slices: this.props.dashboard.slices,
-    });
+  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, oldItem, newItem) {
-    const newSlice = this.props.dashboard.getSlice(newItem.i);
-    if (oldItem.w !== newItem.w || oldItem.h !== newItem.h) {
-      this.setState({ layout }, () => newSlice.resize());
-    }
-    this.props.dashboard.onChange();
+  onResizeStop(layout) {
+    this.props.updateDashboardLayout(layout);
+    this.props.onChange();
   }
 
   onDragStop(layout) {
-    this.setState({ layout });
-    this.props.dashboard.onChange();
+    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;
   }
 
-  removeSlice(sliceId) {
-    $('[data-toggle=tooltip]').tooltip('hide');
-    this.setState({
-      layout: this.state.layout.filter(function (reactPos) {
-        return reactPos.i !== String(sliceId);
-      }),
-      slices: this.state.slices.filter(function (slice) {
-        return slice.slice_id !== sliceId;
-      }),
-    });
-    this.props.dashboard.onChange();
+  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 dashbaord 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.state.slices.map(slice => (slice.slice_id)).indexOf(sliceId);
+    const index = this.findSliceIndexById(sliceId);
     if (index === -1) {
       return;
     }
 
-    // update slice_name first
-    const oldSlices = this.state.slices;
-    const currentSlice = this.state.slices[index];
-    const updated = Object.assign({},
-        this.state.slices[index], { slice_name: sliceName });
-    const updatedSlices = this.state.slices.slice();
-    updatedSlices[index] = updated;
-    this.setState({ slices: updatedSlices });
-
-    const sliceParams = {};
-    sliceParams.slice_id = currentSlice.slice_id;
-    sliceParams.action = 'overwrite';
-    sliceParams.slice_name = sliceName;
-    const saveUrl = getExploreUrl(currentSlice.form_data, 'base', false, null, sliceParams);
-
-    $.ajax({
-      url: saveUrl,
-      type: 'GET',
-      success: () => {
-        notify.success('This slice name was saved successfully.');
-      },
-      error: () => {
-        // if server-side reject the overwrite action,
-        // revert to old state
-        this.setState({ slices: oldSlices });
-        notify.error('You don\'t have the rights to alter this slice');
-      },
-    });
+    const currentSlice = this.props.dashboard.slices[index];
+    if (currentSlice.slice_name === sliceName) {
+      return;
+    }
+
+    this.props.saveSlice(currentSlice, sliceName);
   }
 
-  serialize() {
-    return this.state.layout.map(reactPos => ({
-      slice_id: reactPos.i,
-      col: reactPos.x + 1,
-      row: reactPos.y,
-      size_x: reactPos.w,
-      size_y: reactPos.h,
-    }));
+  isExpanded(slice) {
+    return this.props.dashboard.metadata.expanded_slices &&
+      this.props.dashboard.metadata.expanded_slices[slice.slice_id];
   }
 
   render() {
     return (
       <ResponsiveReactGridLayout
         className="layout"
-        layouts={{ lg: this.state.layout }}
-        onResizeStop={this.onResizeStop.bind(this)}
-        onDragStop={this.onDragStop.bind(this)}
+        layouts={{ lg: this.props.dashboard.layout }}
+        onResizeStop={this.onResizeStop}
+        onDragStop={this.onDragStop}
         cols={{ lg: 12, md: 12, sm: 10, xs: 8, xxs: 6 }}
         rowHeight={100}
         autoSize
@@ -134,19 +140,36 @@ class GridLayout extends React.Component {
         useCSSTransforms
         draggableHandle=".drag"
       >
-        {this.state.slices.map(slice => (
+        {this.props.dashboard.slices.map(slice => (
           <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)}
           >
-            <SliceCell
+            <GridCell
               slice={slice}
-              removeSlice={this.removeSlice.bind(this, slice.slice_id)}
-              expandedSlices={this.props.dashboard.metadata.expanded_slices}
-              updateSliceName={this.props.dashboard.dash_edit_perm ?
-                this.updateSliceName.bind(this) : null}
+              chartKey={'slice_' + slice.slice_id}
+              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)}
+              exploreChartUrl={getExploreUrl(this.props.getFormDataExtra(slice))}
+              exportCSVUrl={getExploreUrl(this.props.getFormDataExtra(slice), 'csv')}
+              isExpanded={!!this.isExpanded(slice)}
+              isLoading={[undefined, 'loading']
+                .indexOf(this.props.charts['slice_' + slice.slice_id].chartStatus) !== -1}
+              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}
             />
           </div>
         ))}
@@ -156,5 +179,6 @@ class GridLayout extends React.Component {
 }
 
 GridLayout.propTypes = propTypes;
+GridLayout.defaultProps = defaultProps;
 
 export default GridLayout;
diff --git a/superset/assets/javascripts/dashboard/components/Header.jsx b/superset/assets/javascripts/dashboard/components/Header.jsx
index a1ab0e8..dfba7e8 100644
--- a/superset/assets/javascripts/dashboard/components/Header.jsx
+++ b/superset/assets/javascripts/dashboard/components/Header.jsx
@@ -3,22 +3,32 @@ import PropTypes from 'prop-types';
 
 import Controls from './Controls';
 import EditableTitle from '../../components/EditableTitle';
+import FaveStar from '../../components/FaveStar';
 
 const propTypes = {
-  dashboard: PropTypes.object,
-};
-const defaultProps = {
+  dashboard: PropTypes.object.isRequired,
+  userId: PropTypes.string.isRequired,
+  isStarred: PropTypes.bool,
+  addSlicesToDashboard: PropTypes.func,
+  onSave: PropTypes.func,
+  onChange: PropTypes.func,
+  fetchFaveStar: PropTypes.func,
+  readFilters: PropTypes.func,
+  renderSlices: PropTypes.func,
+  saveFaveStar: PropTypes.func,
+  serialize: PropTypes.func,
+  startPeriodicRender: PropTypes.func,
+  updateDashboardTitle: PropTypes.func,
 };
 
 class Header extends React.PureComponent {
   constructor(props) {
     super(props);
-    this.state = {
-    };
+
     this.handleSaveTitle = this.handleSaveTitle.bind(this);
   }
   handleSaveTitle(title) {
-    this.props.dashboard.updateDashboardTitle(title);
+    this.props.updateDashboardTitle(title);
   }
   render() {
     const dashboard = this.props.dashboard;
@@ -32,12 +42,29 @@ class Header extends React.PureComponent {
               onSaveTitle={this.handleSaveTitle}
               noPermitTooltip={'You don\'t have the rights to alter this dashboard.'}
             />
-            <span is class="favstar" class_name="Dashboard" obj_id={dashboard.id} />
+            <span className="favstar">
+              <FaveStar
+                itemId={dashboard.id}
+                fetchFaveStar={this.props.fetchFaveStar}
+                saveFaveStar={this.props.saveFaveStar}
+                isStarred={this.props.isStarred}
+              />
+            </span>
           </h1>
         </div>
         <div className="pull-right" style={{ marginTop: '35px' }}>
           {!this.props.dashboard.standalone_mode &&
-          <Controls dashboard={dashboard} />
+          <Controls
+            dashboard={dashboard}
+            userId={this.props.userId}
+            addSlicesToDashboard={this.props.addSlicesToDashboard}
+            onSave={this.props.onSave}
+            onChange={this.props.onChange}
+            readFilters={this.props.readFilters}
+            renderSlices={this.props.renderSlices}
+            serialize={this.props.serialize}
+            startPeriodicRender={this.props.startPeriodicRender}
+          />
         }
         </div>
         <div className="clearfix" />
@@ -46,6 +73,5 @@ class Header extends React.PureComponent {
   }
 }
 Header.propTypes = propTypes;
-Header.defaultProps = defaultProps;
 
 export default Header;
diff --git a/superset/assets/javascripts/dashboard/components/SaveModal.jsx b/superset/assets/javascripts/dashboard/components/SaveModal.jsx
index f35eb63..cc91dae 100644
--- a/superset/assets/javascripts/dashboard/components/SaveModal.jsx
+++ b/superset/assets/javascripts/dashboard/components/SaveModal.jsx
@@ -13,6 +13,9 @@ const propTypes = {
   css: PropTypes.string,
   dashboard: PropTypes.object.isRequired,
   triggerNode: PropTypes.node.isRequired,
+  readFilters: PropTypes.func,
+  serialize: PropTypes.func,
+  onSave: PropTypes.func,
 };
 
 class SaveModal extends React.PureComponent {
@@ -45,8 +48,8 @@ class SaveModal extends React.PureComponent {
     });
   }
   saveDashboardRequest(data, url, saveType) {
-    const dashboard = this.props.dashboard;
     const saveModal = this.modal;
+    const onSaveDashboard = this.props.onSave;
     Object.assign(data, { css: this.props.css });
     $.ajax({
       type: 'POST',
@@ -56,7 +59,7 @@ class SaveModal extends React.PureComponent {
       },
       success(resp) {
         saveModal.close();
-        dashboard.onSave();
+        onSaveDashboard();
         if (saveType === 'newDashboard') {
           window.location = `/superset/dashboard/${resp.id}/`;
         } else {
@@ -72,21 +75,13 @@ class SaveModal extends React.PureComponent {
   }
   saveDashboard(saveType, newDashboardTitle) {
     const dashboard = this.props.dashboard;
-    const expandedSlices = {};
-    $.each($('.slice_info'), function () {
-      const widget = $(this).parents('.widget');
-      const sliceDescription = widget.find('.slice_description');
-      if (sliceDescription.is(':visible')) {
-        expandedSlices[$(widget).attr('data-slice-id')] = true;
-      }
-    });
-    const positions = dashboard.reactGridLayout.serialize();
+    const positions = this.props.serialize();
     const data = {
       positions,
       css: this.state.css,
-      expanded_slices: expandedSlices,
+      expanded_slices: dashboard.metadata.expanded_slices || {},
       dashboard_title: dashboard.dashboard_title,
-      default_filters: dashboard.readFilters(),
+      default_filters: this.props.readFilters(),
       duplicate_slices: this.state.duplicateSlices,
     };
     let url = null;
diff --git a/superset/assets/javascripts/dashboard/components/SliceAdder.jsx b/superset/assets/javascripts/dashboard/components/SliceAdder.jsx
index 4c5f462..03e0cb8 100644
--- a/superset/assets/javascripts/dashboard/components/SliceAdder.jsx
+++ b/superset/assets/javascripts/dashboard/components/SliceAdder.jsx
@@ -11,6 +11,8 @@ 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 {
@@ -43,7 +45,7 @@ class SliceAdder extends React.Component {
   }
 
   onEnterModal() {
-    const uri = '/sliceaddview/api/read?_flt_0_created_by=' + this.props.dashboard.curUserId;
+    const uri = `/sliceaddview/api/read?_flt_0_created_by=${this.props.userId}`;
     this.slicesRequest = $.ajax({
       url: uri,
       type: 'GET',
@@ -52,7 +54,7 @@ class SliceAdder extends React.Component {
         const slices = response.result.map(slice => ({
           id: slice.id,
           sliceName: slice.slice_name,
-          vizType: slice.viz_type,
+          vizType: slice.vizType,
           modified: slice.modified,
         }));
 
@@ -65,14 +67,30 @@ class SliceAdder extends React.Component {
       error: (error) => {
         this.errored = true;
         this.setState({
-          errorMsg: this.props.dashboard.getAjaxErrorMsg(error),
+          errorMsg: t('Sorry, there was an error fetching slices to this dashboard: ') +
+          this.getAjaxErrorMsg(error),
         });
       },
     });
   }
 
+  getAjaxErrorMsg(error) {
+    const respJSON = error.responseJSON;
+    return (respJSON && respJSON.message) ? respJSON.message :
+      error.responseText;
+  }
+
   addSlices() {
-    this.props.dashboard.addSlicesToDashboard(Object.keys(this.state.selectionMap));
+    const adder = this;
+    this.props.addSlicesToDashboard(Object.keys(this.state.selectionMap))
+      // if successful, page will be reloaded.
+      .fail((error) => {
+        adder.errored = true;
+        adder.setState({
+          errorMsg: t('Sorry, there was an error adding slices to this dashboard: ') +
+          this.getAjaxErrorMsg(error),
+        });
+      });
   }
 
   toggleSlice(slice) {
diff --git a/superset/assets/javascripts/dashboard/components/SliceCell.jsx b/superset/assets/javascripts/dashboard/components/SliceCell.jsx
deleted file mode 100644
index 2fbdff3..0000000
--- a/superset/assets/javascripts/dashboard/components/SliceCell.jsx
+++ /dev/null
@@ -1,117 +0,0 @@
-/* eslint-disable react/no-danger */
-import React from 'react';
-import PropTypes from 'prop-types';
-
-import { t } from '../../locales';
-import { getExploreUrl } from '../../explore/exploreUtils';
-import EditableTitle from '../../components/EditableTitle';
-
-const propTypes = {
-  slice: PropTypes.object.isRequired,
-  removeSlice: PropTypes.func.isRequired,
-  updateSliceName: PropTypes.func,
-  expandedSlices: PropTypes.object,
-};
-
-const SliceCell = ({ expandedSlices, removeSlice, slice, updateSliceName }) => {
-  const onSaveTitle = (newTitle) => {
-    if (updateSliceName) {
-      updateSliceName(slice.slice_id, newTitle);
-    }
-  };
-
-  return (
-    <div className="slice-cell" id={`${slice.slice_id}-cell`}>
-      <div className="row chart-header">
-        <div className="col-md-12">
-          <div className="header">
-            <EditableTitle
-              title={slice.slice_name}
-              canEdit={!!updateSliceName}
-              onSaveTitle={onSaveTitle}
-              noPermitTooltip={'You don\'t have the rights to alter this dashboard.'}
-            />
-          </div>
-          <div className="chart-controls">
-            <div id={'controls_' + slice.slice_id} className="pull-right">
-              <a title={t('Move chart')} data-toggle="tooltip">
-                <i className="fa fa-arrows drag" />
-              </a>
-              <a className="refresh" title={t('Force refresh data')} data-toggle="tooltip">
-                <i className="fa fa-repeat" />
-              </a>
-              {slice.description &&
-                <a title={t('Toggle chart description')}>
-                  <i
-                    className="fa fa-info-circle slice_info"
-                    title={slice.description}
-                    data-toggle="tooltip"
-                  />
-                </a>
-              }
-              <a
-                href={slice.edit_url}
-                title={t('Edit chart')}
-                data-toggle="tooltip"
-              >
-                <i className="fa fa-pencil" />
-              </a>
-              <a
-                className="exportCSV"
-                href={getExploreUrl(slice.form_data, 'csv')}
-                title={t('Export CSV')}
-                data-toggle="tooltip"
-              >
-                <i className="fa fa-table" />
-              </a>
-              <a
-                className="exploreChart"
-                href={getExploreUrl(slice.form_data)}
-                title={t('Explore chart')}
-                data-toggle="tooltip"
-              >
-                <i className="fa fa-share" />
-              </a>
-              <a
-                className="remove-chart"
-                title={t('Remove chart from dashboard')}
-                data-toggle="tooltip"
-              >
-                <i
-                  className="fa fa-close"
-                  onClick={() => { removeSlice(slice.slice_id); }}
-                />
-              </a>
-            </div>
-          </div>
-        </div>
-      </div>
-      <div
-        className="slice_description bs-callout bs-callout-default"
-        style={
-          expandedSlices &&
-          expandedSlices[String(slice.slice_id)] ? {} : { display: 'none' }
-        }
-        dangerouslySetInnerHTML={{ __html: slice.description_markeddown }}
-      />
-      <div className="row chart-container">
-        <input type="hidden" value="false" />
-        <div id={'token_' + slice.slice_id} className="token col-md-12">
-          <img
-            src="/static/assets/images/loading.gif"
-            className="loading"
-            alt="loading"
-          />
-          <div
-            id={'con_' + slice.slice_id}
-            className={`slice_container ${slice.form_data.viz_type}`}
-          />
-        </div>
-      </div>
-    </div>
-  );
-};
-
-SliceCell.propTypes = propTypes;
-
-export default SliceCell;
diff --git a/superset/assets/javascripts/dashboard/components/SliceHeader.jsx b/superset/assets/javascripts/dashboard/components/SliceHeader.jsx
new file mode 100644
index 0000000..d1a2d9e
--- /dev/null
+++ b/superset/assets/javascripts/dashboard/components/SliceHeader.jsx
@@ -0,0 +1,142 @@
+import React from 'react';
+import PropTypes from 'prop-types';
+import moment from 'moment';
+
+import { t } from '../../locales';
+import EditableTitle from '../../components/EditableTitle';
+import TooltipWrapper from '../../components/TooltipWrapper';
+
+const propTypes = {
+  slice: PropTypes.object.isRequired,
+  exploreChartUrl: PropTypes.string,
+  exportCSVUrl: PropTypes.string,
+  isExpanded: PropTypes.bool,
+  formDataExtra: PropTypes.object,
+  removeSlice: PropTypes.func,
+  updateSliceName: PropTypes.func,
+  toggleExpandSlice: PropTypes.func,
+  forceRefresh: PropTypes.func,
+};
+
+const defaultProps = {
+  forceRefresh: () => ({}),
+  removeSlice: () => ({}),
+  updateSliceName: () => ({}),
+  toggleExpandSlice: () => ({}),
+};
+
+class SliceHeader extends React.PureComponent {
+  constructor(props) {
+    super(props);
+
+    this.onSaveTitle = this.onSaveTitle.bind(this);
+  }
+
+  onSaveTitle(newTitle) {
+    if (this.props.updateSliceName) {
+      this.props.updateSliceName(this.props.slice.slice_id, newTitle);
+    }
+  }
+
+  render() {
+    const slice = this.props.slice;
+    const isCached = slice.is_cached;
+    const isExpanded = !!this.props.isExpanded;
+    const cachedWhen = moment.utc(slice.cached_dttm).fromNow();
+    const refreshTooltip = isCached ?
+      t('Served from data cached %s . Click to force refresh.', cachedWhen) :
+      t('Force refresh data');
+
+    return (
+      <div className="row chart-header">
+        <div className="col-md-12">
+          <div className="header">
+            <EditableTitle
+              title={slice.slice_name}
+              canEdit={!!this.props.updateSliceName}
+              onSaveTitle={this.onSaveTitle}
+              noPermitTooltip={'You don\'t have the rights to alter this dashboard.'}
+            />
+          </div>
+          <div className="chart-controls">
+            <div id={'controls_' + slice.slice_id} className="pull-right">
+              <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.props.forceRefresh(slice.slice_id))}
+              >
+                <TooltipWrapper
+                  placement="top"
+                  label="refresh"
+                  tooltip={refreshTooltip}
+                >
+                  <i className="fa fa-repeat" />
+                </TooltipWrapper>
+              </a>
+              {slice.description &&
+              <a onClick={() => this.props.toggleExpandSlice(slice, !isExpanded)}>
+                <TooltipWrapper
+                  placement="top"
+                  label="description"
+                  tooltip={t('Toggle chart description')}
+                >
+                  <i className="fa fa-info-circle slice_info" />
+                </TooltipWrapper>
+              </a>
+              }
+              <a href={slice.edit_url} target="_blank">
+                <TooltipWrapper
+                  placement="top"
+                  label="edit"
+                  tooltip={t('Edit chart')}
+                >
+                  <i className="fa fa-pencil" />
+                </TooltipWrapper>
+              </a>
+              <a className="exportCSV" href={this.props.exportCSVUrl}>
+                <TooltipWrapper
+                  placement="top"
+                  label="exportCSV"
+                  tooltip={t('Export CSV')}
+                >
+                  <i className="fa fa-table" />
+                </TooltipWrapper>
+              </a>
+              <a className="exploreChart" href={this.props.exploreChartUrl} target="_blank">
+                <TooltipWrapper
+                  placement="top"
+                  label="exploreChart"
+                  tooltip={t('Explore chart')}
+                >
+                  <i className="fa fa-share" />
+                </TooltipWrapper>
+              </a>
+              <a className="remove-chart" onClick={() => (this.props.removeSlice(slice))}>
+                <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;
+
+export default SliceHeader;
diff --git a/superset/assets/javascripts/dashboard/index.jsx b/superset/assets/javascripts/dashboard/index.jsx
new file mode 100644
index 0000000..774e071
--- /dev/null
+++ b/superset/assets/javascripts/dashboard/index.jsx
@@ -0,0 +1,29 @@
+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/javascripts/dashboard/reducers.js b/superset/assets/javascripts/dashboard/reducers.js
new file mode 100644
index 0000000..8d7b7f4
--- /dev/null
+++ b/superset/assets/javascripts/dashboard/reducers.js
@@ -0,0 +1,188 @@
+import { combineReducers } from 'redux';
+import d3 from 'd3';
+
+import charts, { chart } from '../chart/chartReducer';
+import * as actions from './actions';
+import { getParam } from '../modules/utils';
+import { alterInArr, removeFromArr } from '../reduxUtils';
+import { applyDefaultFormData } from '../explore/stores/store';
+
+export function getInitialState(bootstrapData) {
+  const { user_id, datasources, common } = bootstrapData;
+  delete common.locale;
+  delete common.language_pack;
+
+  const dashboard = { ...bootstrapData.dashboard_data };
+  const filters = {};
+  try {
+    // allow request parameter overwrite dashboard metadata
+    const filterData = JSON.parse(getParam('preselect_filters') || dashboard.metadata.default_filters);
+    for (const key in filterData) {
+      const sliceId = parseInt(key, 10);
+      filters[sliceId] = filterData[key];
+    }
+  } catch (e) {
+    //
+  }
+
+  dashboard.posDict = {};
+  dashboard.layout = [];
+  if (dashboard.position_json) {
+    dashboard.position_json.forEach((position) => {
+      dashboard.posDict[position.slice_id] = position;
+    });
+  }
+  dashboard.slices.forEach((slice, index) => {
+    const sliceId = slice.slice_id;
+    let pos = dashboard.posDict[sliceId];
+    if (!pos) {
+      pos = {
+        col: (index * 4 + 1) % 12,
+        row: Math.floor((index) / 3) * 4,
+        size_x: 4,
+        size_y: 4,
+      };
+    }
+
+    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 },
+  };
+}
+
+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 newLayout = state.dashboard.layout.filter(function (reactPos) {
+        return reactPos.i !== String(action.slice.slice_id);
+      });
+      const newDashboard = removeFromArr(state.dashboard, 'slices', action.slice, 'slice_id');
+      return { ...state, dashboard: { ...newDashboard, layout: newLayout } };
+    },
+    [actions.TOGGLE_FAVE_STAR]() {
+      return { ...state, isStarred: action.isStarred };
+    },
+    [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;
+      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) {
+        if (!(sliceId in state.filters)) {
+          filters = { ...state.filters, [sliceId]: {} };
+        }
+
+        let newFilter = {};
+        if (state.filters[sliceId] && !(col in state.filters[sliceId]) || !merge) {
+          newFilter = { ...state.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 (state.filters[sliceId][col] instanceof Array) {
+          newFilter = d3.merge([state.filters[sliceId][col], vals]);
+        } else {
+          newFilter = d3.merge([[state.filters[sliceId][col]], vals])[0] || '';
+        }
+        filters = { ...state.filters, [sliceId]: newFilter };
+      }
+      return { ...state, filters, refresh };
+    },
+    [actions.CLEAR_FILTER]() {
+      const newFilters = { ...state.filters };
+      delete newFilters[action.sliceId];
+
+      return { ...state.dashboard, filter: newFilters, refresh: true };
+    },
+    [actions.REMOVE_FILTER]() {
+      const newFilters = { ...state.filters };
+      const { sliceId, col, vals } = action;
+
+      if (sliceId in state.filters) {
+        if (col in state.filters[sliceId]) {
+          const a = [];
+          newFilters[sliceId][col].forEach(function (v) {
+            if (vals.indexOf(v) < 0) {
+              a.push(v);
+            }
+          });
+          newFilters[sliceId][col] = a;
+        }
+      }
+      return { ...state.dashboard, filter: newFilters, refresh: true };
+    },
+
+    // slice reducer
+    [actions.UPDATE_SLICE_NAME]() {
+      const newDashboard = alterInArr(
+        state.dashboard, 'slices',
+        action.slice, { slice_name: action.sliceName },
+        'slice_id');
+      return { ...state.dashboard, dashboard: newDashboard };
+    },
+  };
+
+  if (action.type in actionHandlers) {
+    return actionHandlers[action.type]();
+  }
+  return state;
+};
+
+export default combineReducers({
+  charts,
+  dashboard,
+});
diff --git a/superset/assets/javascripts/explore/actions/chartActions.js b/superset/assets/javascripts/explore/actions/chartActions.js
deleted file mode 100644
index beca6ef..0000000
--- a/superset/assets/javascripts/explore/actions/chartActions.js
+++ /dev/null
@@ -1,74 +0,0 @@
-import { getExploreUrl } from '../exploreUtils';
-import { getFormDataFromControls } from '../stores/store';
-import { triggerQuery } from './exploreActions';
-
-const $ = window.$ = require('jquery');
-
-export const CHART_UPDATE_STARTED = 'CHART_UPDATE_STARTED';
-export function chartUpdateStarted(queryRequest, latestQueryFormData) {
-  return { type: CHART_UPDATE_STARTED, queryRequest, latestQueryFormData };
-}
-
-export const CHART_UPDATE_SUCCEEDED = 'CHART_UPDATE_SUCCEEDED';
-export function chartUpdateSucceeded(queryResponse) {
-  return { type: CHART_UPDATE_SUCCEEDED, queryResponse };
-}
-
-export const CHART_UPDATE_STOPPED = 'CHART_UPDATE_STOPPED';
-export function chartUpdateStopped(queryRequest) {
-  if (queryRequest) {
-    queryRequest.abort();
-  }
-  return { type: CHART_UPDATE_STOPPED };
-}
-
-export const CHART_UPDATE_TIMEOUT = 'CHART_UPDATE_TIMEOUT';
-export function chartUpdateTimeout(statusText, timeout) {
-  return { type: CHART_UPDATE_TIMEOUT, statusText, timeout };
-}
-
-export const CHART_UPDATE_FAILED = 'CHART_UPDATE_FAILED';
-export function chartUpdateFailed(queryResponse) {
-  return { type: CHART_UPDATE_FAILED, queryResponse };
-}
-
-export const UPDATE_CHART_STATUS = 'UPDATE_CHART_STATUS';
-export function updateChartStatus(status) {
-  return { type: UPDATE_CHART_STATUS, status };
-}
-
-export const CHART_RENDERING_FAILED = 'CHART_RENDERING_FAILED';
-export function chartRenderingFailed(error) {
-  return { type: CHART_RENDERING_FAILED, error };
-}
-
-export const REMOVE_CHART_ALERT = 'REMOVE_CHART_ALERT';
-export function removeChartAlert() {
-  return { type: REMOVE_CHART_ALERT };
-}
-
-export const RUN_QUERY = 'RUN_QUERY';
-export function runQuery(formData, force = false, timeout = 60) {
-  return function (dispatch, getState) {
-    const { explore } = getState();
-    const lastQueryFormData = getFormDataFromControls(explore.controls);
-    const url = getExploreUrl(formData, 'json', force);
-    const queryRequest = $.ajax({
-      url,
-      dataType: 'json',
-      success(queryResponse) {
-        dispatch(chartUpdateSucceeded(queryResponse));
-      },
-      error(err) {
-        if (err.statusText === 'timeout') {
-          dispatch(chartUpdateTimeout(err.statusText, timeout));
-        } else if (err.statusText !== 'abort') {
-          dispatch(chartUpdateFailed(err.responseJSON));
-        }
-      },
-      timeout: timeout * 1000,
-    });
-    dispatch(chartUpdateStarted(queryRequest, lastQueryFormData));
-    dispatch(triggerQuery(false));
-  };
-}
diff --git a/superset/assets/javascripts/explore/actions/exploreActions.js b/superset/assets/javascripts/explore/actions/exploreActions.js
index dbba7b7..b5be4d3 100644
--- a/superset/assets/javascripts/explore/actions/exploreActions.js
+++ b/superset/assets/javascripts/explore/actions/exploreActions.js
@@ -1,4 +1,5 @@
 /* eslint camelcase: 0 */
+import { triggerQuery } from '../../chart/chartAction';
 
 const $ = window.$ = require('jquery');
 
@@ -54,11 +55,6 @@ export function resetControls() {
   return { type: RESET_FIELDS };
 }
 
-export const TRIGGER_QUERY = 'TRIGGER_QUERY';
-export function triggerQuery(value = true) {
-  return { type: TRIGGER_QUERY, value };
-}
-
 export function fetchDatasourceMetadata(datasourceKey, alsoTriggerQuery = false) {
   return function (dispatch) {
     dispatch(fetchDatasourceStarted());
@@ -146,11 +142,6 @@ export function updateChartTitle(slice_name) {
   return { type: UPDATE_CHART_TITLE, slice_name };
 }
 
-export const RENDER_TRIGGERED = 'RENDER_TRIGGERED';
-export function renderTriggered() {
-  return { type: RENDER_TRIGGERED };
-}
-
 export const CREATE_NEW_SLICE = 'CREATE_NEW_SLICE';
 export function createNewSlice(can_add, can_download, can_overwrite, slice, form_data) {
   return { type: CREATE_NEW_SLICE, can_add, can_download, can_overwrite, slice, form_data };
diff --git a/superset/assets/javascripts/explore/components/ChartContainer.jsx b/superset/assets/javascripts/explore/components/ChartContainer.jsx
deleted file mode 100644
index f3c660a..0000000
--- a/superset/assets/javascripts/explore/components/ChartContainer.jsx
+++ /dev/null
@@ -1,362 +0,0 @@
-import $ from 'jquery';
-import React from 'react';
-import PropTypes from 'prop-types';
-import Mustache from 'mustache';
-import { connect } from 'react-redux';
-import { Alert, Collapse, Panel } from 'react-bootstrap';
-import visMap from '../../../visualizations/main';
-import { d3format } from '../../modules/utils';
-import ExploreActionButtons from './ExploreActionButtons';
-import EditableTitle from '../../components/EditableTitle';
-import FaveStar from '../../components/FaveStar';
-import TooltipWrapper from '../../components/TooltipWrapper';
-import Timer from '../../components/Timer';
-import { getExploreUrl } from '../exploreUtils';
-import { getFormDataFromControls } from '../stores/store';
-import CachedLabel from '../../components/CachedLabel';
-import { t } from '../../locales';
-
-const CHART_STATUS_MAP = {
-  failed: 'danger',
-  loading: 'warning',
-  success: 'success',
-};
-
-const propTypes = {
-  actions: PropTypes.object.isRequired,
-  alert: PropTypes.string,
-  can_overwrite: PropTypes.bool.isRequired,
-  can_download: PropTypes.bool.isRequired,
-  chartStatus: PropTypes.string,
-  chartUpdateEndTime: PropTypes.number,
-  chartUpdateStartTime: PropTypes.number.isRequired,
-  column_formats: PropTypes.object,
-  containerId: PropTypes.string.isRequired,
-  height: PropTypes.string.isRequired,
-  width: PropTypes.string.isRequired,
-  isStarred: PropTypes.bool.isRequired,
-  slice: PropTypes.object,
-  table_name: PropTypes.string,
-  viz_type: PropTypes.string.isRequired,
-  formData: PropTypes.object,
-  latestQueryFormData: PropTypes.object,
-  queryResponse: PropTypes.object,
-  triggerRender: PropTypes.bool,
-  standalone: PropTypes.bool,
-  datasourceType: PropTypes.string,
-  datasourceId: PropTypes.number,
-  timeout: PropTypes.number,
-};
-
-class ChartContainer extends React.PureComponent {
-  constructor(props) {
-    super(props);
-    this.state = {
-      selector: `#${props.containerId}`,
-      showStackTrace: false,
-    };
-  }
-
-  componentDidUpdate(prevProps) {
-    if (
-        this.props.queryResponse &&
-        (
-          prevProps.queryResponse !== this.props.queryResponse ||
-          prevProps.height !== this.props.height ||
-          prevProps.width !== this.props.width ||
-          this.props.triggerRender
-        ) && !this.props.queryResponse.error
-        && this.props.chartStatus !== 'failed'
-        && this.props.chartStatus !== 'stopped'
-        && this.props.chartStatus !== 'loading'
-      ) {
-      this.renderViz();
-    }
-  }
-
-  getMockedSliceObject() {
-    const props = this.props;
-    const getHeight = () => {
-      const headerHeight = props.standalone ? 0 : 100;
-      return parseInt(props.height, 10) - headerHeight;
-    };
-    return {
-      viewSqlQuery: props.queryResponse.query,
-      containerId: props.containerId,
-      datasource: props.datasource,
-      selector: this.state.selector,
-      formData: props.formData,
-      container: {
-        html: (data) => {
-          // this should be a callback to clear the contents of the slice container
-          $(this.state.selector).html(data);
-        },
-        css: (property, value) => {
-          $(this.state.selector).css(property, value);
-        },
-        height: getHeight,
-        show: () => { },
-        get: n => ($(this.state.selector).get(n)),
-        find: classname => ($(this.state.selector).find(classname)),
-      },
-
-      width: () => this.chartContainerRef.getBoundingClientRect().width,
-
-      height: getHeight,
-
-      render_template: (s) => {
-        const context = {
-          width: this.width,
-          height: this.height,
-        };
-        return Mustache.render(s, context);
-      },
-
-      setFilter: () => {},
-
-      getFilters: () => (
-        // return filter objects from viz.formData
-        {}
-      ),
-
-      addFilter: () => {},
-
-      removeFilter: () => {},
-
-      done: () => {},
-      clearError: () => {
-        // no need to do anything here since Alert is closable
-        // query button will also remove Alert
-      },
-      error() {},
-
-      d3format: (col, number) => {
-        // mock d3format function in Slice object in superset.js
-        const format = props.column_formats[col];
-        return d3format(format, number);
-      },
-
-      data: {
-        csv_endpoint: getExploreUrl(props.formData, 'csv'),
-        json_endpoint: getExploreUrl(props.formData, 'json'),
-        standalone_endpoint: getExploreUrl(props.formData, 'standalone'),
-      },
-
-    };
-  }
-
-  removeAlert() {
-    this.props.actions.removeChartAlert();
-  }
-
-  runQuery() {
-    this.props.actions.runQuery(this.props.formData, true, this.props.timeout);
-  }
-
-  updateChartTitleOrSaveSlice(newTitle) {
-    const isNewSlice = !this.props.slice;
-    const params = {
-      slice_name: newTitle,
-      action: isNewSlice ? 'saveas' : 'overwrite',
-    };
-    const saveUrl = getExploreUrl(this.props.formData, 'base', false, null, params);
-    this.props.actions.saveSlice(saveUrl)
-      .then((data) => {
-        if (isNewSlice) {
-          this.props.actions.createNewSlice(
-              data.can_add, data.can_download, data.can_overwrite,
-              data.slice, data.form_data);
-        } else {
-          this.props.actions.updateChartTitle(newTitle);
-        }
-      });
-  }
-
-  renderChartTitle() {
-    let title;
-    if (this.props.slice) {
-      title = this.props.slice.slice_name;
-    } else {
-      title = t('%s - untitled', this.props.table_name);
-    }
-    return title;
-  }
-
-  renderViz() {
-    this.props.actions.renderTriggered();
-    const mockSlice = this.getMockedSliceObject();
-    this.setState({ mockSlice });
-    const viz = visMap[this.props.viz_type];
-    try {
-      viz(mockSlice, this.props.queryResponse, this.props.actions.setControlValue);
-    } catch (e) {
-      this.props.actions.chartRenderingFailed(e);
-    }
-  }
-
-  renderAlert() {
-    /* eslint-disable react/no-danger */
-    const msg = (
-      <div>
-        <i
-          className="fa fa-close pull-right"
-          onClick={this.removeAlert.bind(this)}
-          style={{ cursor: 'pointer' }}
-        />
-        <p
-          dangerouslySetInnerHTML={{ __html: this.props.alert }}
-        />
-      </div>);
-    return (
-      <div>
-        <Alert
-          bsStyle="warning"
-          onClick={() => this.setState({ showStackTrace: !this.state.showStackTrace })}
-        >
-          {msg}
-        </Alert>
-        {this.props.queryResponse && this.props.queryResponse.stacktrace &&
-          <Collapse in={this.state.showStackTrace}>
-            <pre>
-              {this.props.queryResponse.stacktrace}
-            </pre>
-          </Collapse>
-        }
-      </div>);
-  }
-
-  renderChart() {
-    if (this.props.alert) {
-      return this.renderAlert();
-    }
-    const loading = this.props.chartStatus === 'loading';
-    return (
-      <div>
-        {loading &&
-          <img
-            alt="loading"
-            width="25"
-            src="/static/assets/images/loading.gif"
-            style={{ position: 'absolute' }}
-          />
-        }
-        <div
-          id={this.props.containerId}
-          ref={(ref) => { this.chartContainerRef = ref; }}
-          className={this.props.viz_type}
-          style={{
-            opacity: loading ? '0.25' : '1',
-          }}
-        />
-      </div>
-    );
-  }
-
-  render() {
-    if (this.props.standalone) {
-      // dom manipulation hack to get rid of the boostrap theme's body background
-      $('body').addClass('background-transparent');
-      return this.renderChart();
-    }
-    const queryResponse = this.props.queryResponse;
-    return (
-      <div className="chart-container">
-        <Panel
-          style={{ height: this.props.height }}
-          header={
-            <div
-              id="slice-header"
-              className="clearfix panel-title-large"
-            >
-              <EditableTitle
-                title={this.renderChartTitle()}
-                canEdit={!this.props.slice || this.props.can_overwrite}
-                onSaveTitle={this.updateChartTitleOrSaveSlice.bind(this)}
-              />
-
-              {this.props.slice &&
-                <span>
-                  <FaveStar
-                    sliceId={this.props.slice.slice_id}
-                    actions={this.props.actions}
-                    isStarred={this.props.isStarred}
-                  />
-
-                  <TooltipWrapper
-                    label="edit-desc"
-                    tooltip={t('Edit slice properties')}
-                  >
-                    <a
-                      className="edit-desc-icon"
-                      href={`/slicemodelview/edit/${this.props.slice.slice_id}`}
-                    >
-                      <i className="fa fa-edit" />
-                    </a>
-                  </TooltipWrapper>
-                </span>
-              }
-
-              <div className="pull-right">
-                {this.props.chartStatus === 'success' &&
-                this.props.queryResponse &&
-                this.props.queryResponse.is_cached &&
-                  <CachedLabel
-                    onClick={this.runQuery.bind(this)}
-                    cachedTimestamp={queryResponse.cached_dttm}
-                  />
-                }
-                <Timer
-                  startTime={this.props.chartUpdateStartTime}
-                  endTime={this.props.chartUpdateEndTime}
-                  isRunning={this.props.chartStatus === 'loading'}
-                  status={CHART_STATUS_MAP[this.props.chartStatus]}
-                  style={{ fontSize: '10px', marginRight: '5px' }}
-                />
-                <ExploreActionButtons
-                  slice={this.state.mockSlice}
-                  canDownload={this.props.can_download}
-                  chartStatus={this.props.chartStatus}
-                  queryResponse={queryResponse}
-                  queryEndpoint={getExploreUrl(this.props.latestQueryFormData, 'query')}
-                />
-              </div>
-            </div>
-          }
-        >
-          {this.renderChart()}
-        </Panel>
-      </div>
-    );
-  }
-}
-
-ChartContainer.propTypes = propTypes;
-
-function mapStateToProps({ explore, chart }) {
-  const formData = getFormDataFromControls(explore.controls);
-  return {
-    alert: chart.chartAlert,
-    can_overwrite: !!explore.can_overwrite,
-    can_download: !!explore.can_download,
-    datasource: explore.datasource,
-    column_formats: explore.datasource ? explore.datasource.column_formats : null,
-    containerId: explore.slice ? `slice-container-${explore.slice.slice_id}` : 'slice-container',
-    formData,
-    isStarred: explore.isStarred,
-    slice: explore.slice,
-    standalone: explore.standalone,
-    table_name: formData.datasource_name,
-    viz_type: formData.viz_type,
-    triggerRender: explore.triggerRender,
-    datasourceType: explore.datasource.type,
-    datasourceId: explore.datasource_id,
-    chartStatus: chart.chartStatus,
-    chartUpdateEndTime: chart.chartUpdateEndTime,
-    chartUpdateStartTime: chart.chartUpdateStartTime,
-    latestQueryFormData: chart.latestQueryFormData,
-    queryResponse: chart.queryResponse,
-    timeout: explore.common.conf.SUPERSET_WEBSERVER_TIMEOUT,
-  };
-}
-
-export default connect(mapStateToProps, () => ({}))(ChartContainer);
diff --git a/superset/assets/javascripts/explore/components/EmbedCodeButton.jsx b/superset/assets/javascripts/explore/components/EmbedCodeButton.jsx
index c5615dc..01d59e1 100644
--- a/superset/assets/javascripts/explore/components/EmbedCodeButton.jsx
+++ b/superset/assets/javascripts/explore/components/EmbedCodeButton.jsx
@@ -73,7 +73,7 @@ export default class EmbedCodeButton extends React.Component {
             <div className="col-md-6 col-sm-12">
               <div className="form-group">
                 <small>
-                  <label className="control-label" htmlFor="embed-height">t('Height')</label>
+                  <label className="control-label" htmlFor="embed-height">{t('Height')}</label>
                 </small>
                 <input
                   className="form-control input-sm"
@@ -87,7 +87,7 @@ export default class EmbedCodeButton extends React.Component {
             <div className="col-md-6 col-sm-12">
               <div className="form-group">
                 <small>
-                  <label className="control-label" htmlFor="embed-width">t('Width')</label>
+                  <label className="control-label" htmlFor="embed-width">{t('Width')}</label>
                 </small>
                 <input
                   className="form-control input-sm"
diff --git a/superset/assets/javascripts/explore/components/ExploreChartHeader.jsx b/superset/assets/javascripts/explore/components/ExploreChartHeader.jsx
new file mode 100644
index 0000000..8c2e1f3
--- /dev/null
+++ b/superset/assets/javascripts/explore/components/ExploreChartHeader.jsx
@@ -0,0 +1,140 @@
+import React from 'react';
+import PropTypes from 'prop-types';
+
+import { chartPropType } from '../../chart/chartReducer';
+import ExploreActionButtons from './ExploreActionButtons';
+import EditableTitle from '../../components/EditableTitle';
+import FaveStar from '../../components/FaveStar';
+import TooltipWrapper from '../../components/TooltipWrapper';
+import Timer from '../../components/Timer';
+import { getExploreUrl } from '../exploreUtils';
+import CachedLabel from '../../components/CachedLabel';
+import { t } from '../../locales';
+
+const CHART_STATUS_MAP = {
+  failed: 'danger',
+  loading: 'warning',
+  success: 'success',
+};
+
+const propTypes = {
+  actions: PropTypes.object.isRequired,
+  can_overwrite: PropTypes.bool.isRequired,
+  can_download: PropTypes.bool.isRequired,
+  isStarred: PropTypes.bool.isRequired,
+  slice: PropTypes.object,
+  table_name: PropTypes.string,
+  form_data: PropTypes.object,
+  timeout: PropTypes.number,
+  chart: PropTypes.shape(chartPropType),
+};
+
+class ExploreChartHeader extends React.PureComponent {
+  runQuery() {
+    this.props.actions.runQuery(this.props.form_data, true,
+      this.props.timeout, this.props.chart.chartKey);
+  }
+
+  updateChartTitleOrSaveSlice(newTitle) {
+    const isNewSlice = !this.props.slice;
+    const params = {
+      slice_name: newTitle,
+      action: isNewSlice ? 'saveas' : 'overwrite',
+    };
+    const saveUrl = getExploreUrl(this.props.form_data, 'base', false, null, params);
+    this.props.actions.saveSlice(saveUrl)
+      .then((data) => {
+        if (isNewSlice) {
+          this.props.actions.createNewSlice(
+            data.can_add, data.can_download, data.can_overwrite,
+            data.slice, data.form_data);
+        } else {
+          this.props.actions.updateChartTitle(newTitle);
+        }
+      });
+  }
+
+  renderChartTitle() {
+    let title;
+    if (this.props.slice) {
+      title = this.props.slice.slice_name;
+    } else {
+      title = t('%s - untitled', this.props.table_name);
+    }
+    return title;
+  }
+
+  render() {
+    const queryResponse = this.props.chart.queryResponse;
+    const data = {
+      csv_endpoint: getExploreUrl(this.props.form_data, 'csv'),
+      json_endpoint: getExploreUrl(this.props.form_data, 'json'),
+      standalone_endpoint: getExploreUrl(this.props.form_data, 'standalone'),
+    };
+
+    return (
+      <div
+        id="slice-header"
+        className="clearfix panel-title-large"
+      >
+        <EditableTitle
+          title={this.renderChartTitle()}
+          canEdit={!this.props.slice || this.props.can_overwrite}
+          onSaveTitle={this.updateChartTitleOrSaveSlice.bind(this)}
+        />
+
+        {this.props.slice &&
+        <span>
+          <FaveStar
+            itemId={this.props.slice.slice_id}
+            fetchFaveStar={this.props.actions.fetchFaveStar}
+            saveFaveStar={this.props.actions.saveFaveStar}
+            isStarred={this.props.isStarred}
+          />
+
+          <TooltipWrapper
+            label="edit-desc"
+            tooltip={t('Edit slice properties')}
+          >
+            <a
+              className="edit-desc-icon"
+              href={`/slicemodelview/edit/${this.props.slice.slice_id}`}
+            >
+              <i className="fa fa-edit" />
+            </a>
+          </TooltipWrapper>
+        </span>
+        }
+
+        <div className="pull-right">
+          {this.props.chart.chartStatus === 'success' &&
+          queryResponse &&
+          queryResponse.is_cached &&
+          <CachedLabel
+            onClick={this.runQuery.bind(this)}
+            cachedTimestamp={queryResponse.cached_dttm}
+          />
+          }
+          <Timer
+            startTime={this.props.chart.chartUpdateStartTime}
+            endTime={this.props.chart.chartUpdateEndTime}
+            isRunning={this.props.chart.chartStatus === 'loading'}
+            status={CHART_STATUS_MAP[this.props.chart.chartStatus]}
+            style={{ fontSize: '10px', marginRight: '5px' }}
+          />
+          <ExploreActionButtons
+            slice={Object.assign({}, this.props.slice, { data })}
+            canDownload={this.props.can_download}
+            chartStatus={this.props.chart.chartStatus}
+            queryResponse={queryResponse}
+            queryEndpoint={getExploreUrl(this.props.form_data, 'query')}
+          />
+        </div>
+      </div>
+    );
+  }
+}
+
+ExploreChartHeader.propTypes = propTypes;
+
+export default ExploreChartHeader;
diff --git a/superset/assets/javascripts/explore/components/ExploreChartPanel.jsx b/superset/assets/javascripts/explore/components/ExploreChartPanel.jsx
new file mode 100644
index 0000000..7834787
--- /dev/null
+++ b/superset/assets/javascripts/explore/components/ExploreChartPanel.jsx
@@ -0,0 +1,79 @@
+import $ from 'jquery';
+import React from 'react';
+import PropTypes from 'prop-types';
+import { Panel } from 'react-bootstrap';
+
+import { chartPropType } from '../../chart/chartReducer';
+import ChartContainer from '../../chart/ChartContainer';
+import ExploreChartHeader from './ExploreChartHeader';
+
+const propTypes = {
+  actions: PropTypes.object.isRequired,
+  can_overwrite: PropTypes.bool.isRequired,
+  can_download: PropTypes.bool.isRequired,
+  datasource: PropTypes.object,
+  column_formats: PropTypes.object,
+  containerId: PropTypes.string.isRequired,
+  height: PropTypes.string.isRequired,
+  width: PropTypes.string.isRequired,
+  isStarred: PropTypes.bool.isRequired,
+  slice: PropTypes.object,
+  table_name: PropTypes.string,
+  vizType: PropTypes.string.isRequired,
+  form_data: PropTypes.object,
+  standalone: PropTypes.bool,
+  timeout: PropTypes.number,
+  chart: PropTypes.shape(chartPropType),
+};
+
+class ExploreChartPanel extends React.PureComponent {
+  getHeight() {
+    const headerHeight = this.props.standalone ? 0 : 100;
+    return parseInt(this.props.height, 10) - headerHeight;
+  }
+
+  render() {
+    if (this.props.standalone) {
+      // dom manipulation hack to get rid of the boostrap theme's body background
+      $('body').addClass('background-transparent');
+      return this.renderChart();
+    }
+
+    const header = (
+      <ExploreChartHeader
+        actions={this.props.actions}
+        can_overwrite={this.props.can_overwrite}
+        can_download={this.props.can_download}
+        isStarred={this.props.isStarred}
+        slice={this.props.slice}
+        table_name={this.props.table_name}
+        form_data={this.props.form_data}
+        timeout={this.props.timeout}
+        chart={this.props.chart}
+      />);
+    return (
+      <div className="chart-container">
+        <Panel
+          style={{ height: this.props.height }}
+          header={header}
+        >
+          <ChartContainer
+            containerId={this.props.containerId}
+            datasource={this.props.datasource}
+            formData={this.props.form_data}
+            height={this.getHeight()}
+            slice={this.props.slice}
+            chartKey={this.props.chart.chartKey}
+            setControlValue={this.props.actions.setControlValue}
+            timeout={this.props.timeout}
+            vizType={this.props.vizType}
+          />
+        </Panel>
+      </div>
+    );
+  }
+}
+
+ExploreChartPanel.propTypes = propTypes;
+
+export default ExploreChartPanel;
diff --git a/superset/assets/javascripts/explore/components/ExploreViewContainer.jsx b/superset/assets/javascripts/explore/components/ExploreViewContainer.jsx
index f696ed6..e3ea7f2 100644
--- a/superset/assets/javascripts/explore/components/ExploreViewContainer.jsx
+++ b/superset/assets/javascripts/explore/components/ExploreViewContainer.jsx
@@ -3,27 +3,28 @@ import React from 'react';
 import PropTypes from 'prop-types';
 import { bindActionCreators } from 'redux';
 import { connect } from 'react-redux';
-import ChartContainer from './ChartContainer';
+
+import ExploreChartPanel from './ExploreChartPanel';
 import ControlPanelsContainer from './ControlPanelsContainer';
 import SaveModal from './SaveModal';
 import QueryAndSaveBtns from './QueryAndSaveBtns';
 import { getExploreUrl } from '../exploreUtils';
 import { getFormDataFromControls } from '../stores/store';
+import { chartPropType } from '../../chart/chartReducer';
 import * as exploreActions from '../actions/exploreActions';
 import * as saveModalActions from '../actions/saveModalActions';
-import * as chartActions from '../actions/chartActions';
+import * as chartActions from '../../chart/chartAction';
 
 const propTypes = {
   actions: PropTypes.object.isRequired,
   datasource_type: PropTypes.string.isRequired,
   isDatasourceMetaLoading: PropTypes.bool.isRequired,
   chartStatus: PropTypes.string,
+  chart: PropTypes.shape(chartPropType).isRequired,
   controls: PropTypes.object.isRequired,
   forcedHeight: PropTypes.string,
   form_data: PropTypes.object.isRequired,
   standalone: PropTypes.bool.isRequired,
-  triggerQuery: PropTypes.bool.isRequired,
-  queryRequest: PropTypes.object,
   timeout: PropTypes.number,
 };
 
@@ -39,13 +40,12 @@ class ExploreViewContainer extends React.Component {
 
   componentDidMount() {
     window.addEventListener('resize', this.handleResize.bind(this));
-    this.triggerQueryIfNeeded();
   }
 
   componentWillReceiveProps(np) {
     if (np.controls.viz_type.value !== this.props.controls.viz_type.value) {
       this.props.actions.resetControls();
-      this.props.actions.triggerQuery();
+      this.props.actions.triggerQuery(true, this.props.chart.chartKey);
     }
     if (np.controls.datasource.value !== this.props.controls.datasource.value) {
       this.props.actions.fetchDatasourceMetadata(np.form_data.datasource, true);
@@ -63,9 +63,7 @@ class ExploreViewContainer extends React.Component {
   onQuery() {
     // remove alerts when query
     this.props.actions.removeControlPanelAlert();
-    this.props.actions.removeChartAlert();
-
-    this.props.actions.triggerQuery();
+    this.props.actions.triggerQuery(true, this.props.chart.chartKey);
 
     history.pushState(
       {},
@@ -74,7 +72,7 @@ class ExploreViewContainer extends React.Component {
   }
 
   onStop() {
-    this.props.actions.chartUpdateStopped(this.props.queryRequest);
+    this.props.actions.chartUpdateStopped(this.props.chart.queryRequest);
   }
 
   getWidth() {
@@ -90,8 +88,9 @@ class ExploreViewContainer extends React.Component {
   }
 
   triggerQueryIfNeeded() {
-    if (this.props.triggerQuery && !this.hasErrors()) {
-      this.props.actions.runQuery(this.props.form_data, false, this.props.timeout);
+    if (this.props.chart.triggerQuery && !this.hasErrors()) {
+      this.props.actions.runQuery(this.props.form_data, false,
+        this.props.timeout, this.props.chart.chartKey);
     }
   }
 
@@ -134,10 +133,10 @@ class ExploreViewContainer extends React.Component {
   }
   renderChartContainer() {
     return (
-      <ChartContainer
-        actions={this.props.actions}
+      <ExploreChartPanel
         width={this.state.width}
         height={this.state.height}
+        {...this.props}
       />);
   }
 
@@ -168,7 +167,7 @@ class ExploreViewContainer extends React.Component {
               onQuery={this.onQuery.bind(this)}
               onSave={this.toggleModal.bind(this)}
               onStop={this.onStop.bind(this)}
-              loading={this.props.chartStatus === 'loading'}
+              loading={this.props.chart.chartStatus === 'loading'}
               errorMessage={this.renderErrorMessage()}
             />
             <br />
@@ -191,18 +190,28 @@ class ExploreViewContainer extends React.Component {
 
 ExploreViewContainer.propTypes = propTypes;
 
-function mapStateToProps({ explore, chart }) {
+function mapStateToProps({ explore, charts }) {
   const form_data = getFormDataFromControls(explore.controls);
+  const chartKey = Object.keys(charts)[0];
+  const chart = charts[chartKey];
   return {
     isDatasourceMetaLoading: explore.isDatasourceMetaLoading,
+    datasource: explore.datasource,
     datasource_type: explore.datasource.type,
+    datasourceId: explore.datasource_id,
     controls: explore.controls,
+    can_overwrite: !!explore.can_overwrite,
+    can_download: !!explore.can_download,
+    column_formats: explore.datasource ? explore.datasource.column_formats : null,
+    containerId: explore.slice ? `slice-container-${explore.slice.slice_id}` : 'slice-container',
+    isStarred: explore.isStarred,
+    slice: explore.slice,
     form_data,
+    table_name: form_data.datasource_name,
+    vizType: form_data.viz_type,
     standalone: explore.standalone,
-    triggerQuery: explore.triggerQuery,
     forcedHeight: explore.forced_height,
-    queryRequest: chart.queryRequest,
-    chartStatus: chart.chartStatus,
+    chart,
     timeout: explore.common.conf.SUPERSET_WEBSERVER_TIMEOUT,
   };
 }
diff --git a/superset/assets/javascripts/explore/components/SaveModal.jsx b/superset/assets/javascripts/explore/components/SaveModal.jsx
index 2939f2e..7b375c2 100644
--- a/superset/assets/javascripts/explore/components/SaveModal.jsx
+++ b/superset/assets/javascripts/explore/components/SaveModal.jsx
@@ -13,7 +13,7 @@ const propTypes = {
   onHide: PropTypes.func.isRequired,
   actions: PropTypes.object.isRequired,
   form_data: PropTypes.object,
-  user_id: PropTypes.string.isRequired,
+  userId: PropTypes.string.isRequired,
   dashboards: PropTypes.array.isRequired,
   alert: PropTypes.string,
   slice: PropTypes.object,
@@ -34,7 +34,7 @@ class SaveModal extends React.Component {
     };
   }
   componentDidMount() {
-    this.props.actions.fetchDashboards(this.props.user_id);
+    this.props.actions.fetchDashboards(this.props.userId);
   }
   onChange(name, event) {
     switch (name) {
@@ -243,7 +243,7 @@ function mapStateToProps({ explore, saveModal }) {
     datasource: explore.datasource,
     slice: explore.slice,
     can_overwrite: explore.can_overwrite,
-    user_id: explore.user_id,
+    userId: explore.userId,
     dashboards: saveModal.dashboards,
     alert: saveModal.saveModalAlert,
   };
diff --git a/superset/assets/javascripts/explore/index.jsx b/superset/assets/javascripts/explore/index.jsx
index 049e731..2247019 100644
--- a/superset/assets/javascripts/explore/index.jsx
+++ b/superset/assets/javascripts/explore/index.jsx
@@ -34,19 +34,23 @@ const bootstrappedState = Object.assign(
     filterColumnOpts: [],
     isDatasourceMetaLoading: false,
     isStarred: false,
-    triggerQuery: true,
-    triggerRender: false,
   },
 );
 
+const chartKey = bootstrappedState.slice ? ('slice_' + bootstrappedState.slice.slice_id) : 'slice';
 const initState = {
-  chart: {
-    chartAlert: null,
-    chartStatus: null,
-    chartUpdateEndTime: null,
-    chartUpdateStartTime: now(),
-    latestQueryFormData: getFormDataFromControls(controls),
-    queryResponse: null,
+  charts: {
+    [chartKey]: {
+      chartKey,
+      chartAlert: null,
+      chartStatus: null,
+      chartUpdateEndTime: null,
+      chartUpdateStartTime: now(),
+      latestQueryFormData: getFormDataFromControls(controls),
+      queryResponse: null,
+      triggerQuery: true,
+      triggerRender: false,
+    },
   },
   saveModal: {
     dashboards: [],
diff --git a/superset/assets/javascripts/explore/reducers/chartReducer.js b/superset/assets/javascripts/explore/reducers/chartReducer.js
deleted file mode 100644
index 808d884..0000000
--- a/superset/assets/javascripts/explore/reducers/chartReducer.js
+++ /dev/null
@@ -1,80 +0,0 @@
-/* eslint camelcase: 0 */
-import { now } from '../../modules/dates';
-import * as actions from '../actions/chartActions';
-import { t } from '../../locales';
-
-export default function chartReducer(state = {}, action) {
-  const actionHandlers = {
-    [actions.CHART_UPDATE_SUCCEEDED]() {
-      return Object.assign(
-        {},
-        state,
-        {
-          chartStatus: 'success',
-          queryResponse: action.queryResponse,
-        },
-      );
-    },
-    [actions.CHART_UPDATE_STARTED]() {
-      return Object.assign({}, state,
-        {
-          chartStatus: 'loading',
-          chartUpdateEndTime: null,
-          chartUpdateStartTime: now(),
-          queryRequest: action.queryRequest,
-          latestQueryFormData: action.latestQueryFormData,
-        });
-    },
-    [actions.CHART_UPDATE_STOPPED]() {
-      return Object.assign({}, state,
-        {
-          chartStatus: 'stopped',
-          chartAlert: t('Updating chart was stopped'),
-        });
-    },
-    [actions.CHART_RENDERING_FAILED]() {
-      return Object.assign({}, state, {
-        chartStatus: 'failed',
-        chartAlert: t('An error occurred while rendering the visualization: %s', action.error),
-      });
-    },
-    [actions.CHART_UPDATE_TIMEOUT]() {
-      return Object.assign({}, state, {
-        chartStatus: 'failed',
-        chartAlert: (
-          '<strong>Query timeout</strong> - visualization query 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 to 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]() {
-      return Object.assign({}, state, {
-        chartStatus: 'failed',
-        chartAlert: action.queryResponse ? action.queryResponse.error : t('Network error.'),
-        chartUpdateEndTime: now(),
-        queryResponse: action.queryResponse,
-      });
-    },
-    [actions.UPDATE_CHART_STATUS]() {
-      const newState = Object.assign({}, state, { chartStatus: action.status });
-      if (action.status === 'success' || action.status === 'failed') {
-        newState.chartUpdateEndTime = now();
-      }
-      return newState;
-    },
-    [actions.REMOVE_CHART_ALERT]() {
-      if (state.chartAlert !== null) {
-        return Object.assign({}, state, { chartAlert: null });
-      }
-      return state;
-    },
-  };
-
-  if (action.type in actionHandlers) {
-    return actionHandlers[action.type]();
-  }
-  return state;
-}
diff --git a/superset/assets/javascripts/explore/reducers/exploreReducer.js b/superset/assets/javascripts/explore/reducers/exploreReducer.js
index e37df6e..7b55748 100644
--- a/superset/assets/javascripts/explore/reducers/exploreReducer.js
+++ b/superset/assets/javascripts/explore/reducers/exploreReducer.js
@@ -56,11 +56,6 @@ export default function exploreReducer(state = {}, action) {
       }
       return Object.assign({}, state, changes);
     },
-    [actions.TRIGGER_QUERY]() {
-      return Object.assign({}, state, {
-        triggerQuery: action.value,
-      });
-    },
     [actions.UPDATE_CHART_TITLE]() {
       const updatedSlice = Object.assign({}, state.slice, { slice_name: action.slice_name });
       return Object.assign({}, state, { slice: updatedSlice });
@@ -69,9 +64,6 @@ export default function exploreReducer(state = {}, action) {
       const controls = getControlsState(state, getFormDataFromControls(state.controls));
       return Object.assign({}, state, { controls });
     },
-    [actions.RENDER_TRIGGERED]() {
-      return Object.assign({}, state, { triggerRender: false });
-    },
     [actions.CREATE_NEW_SLICE]() {
       return Object.assign({}, state, {
         slice: action.slice,
diff --git a/superset/assets/javascripts/explore/reducers/index.js b/superset/assets/javascripts/explore/reducers/index.js
index 0d5acb0..22f7e8f 100644
--- a/superset/assets/javascripts/explore/reducers/index.js
+++ b/superset/assets/javascripts/explore/reducers/index.js
@@ -1,11 +1,11 @@
 import { combineReducers } from 'redux';
 
-import chart from './chartReducer';
+import charts from '../../chart/chartReducer';
 import saveModal from './saveModalReducer';
 import explore from './exploreReducer';
 
 export default combineReducers({
-  chart,
+  charts,
   saveModal,
   explore,
 });
diff --git a/superset/assets/javascripts/modules/utils.js b/superset/assets/javascripts/modules/utils.js
index 83ec2c0..e7757d4 100644
--- a/superset/assets/javascripts/modules/utils.js
+++ b/superset/assets/javascripts/modules/utils.js
@@ -240,3 +240,11 @@ export function tryNumify(s) {
   }
   return n;
 }
+
+export function getParam(name) {
+  /* eslint no-useless-escape: 0 */
+  const formattedName = name.replace(/[\[]/, '\\[').replace(/[\]]/, '\\]');
+  const regex = new RegExp('[\\?&]' + formattedName + '=([^&#]*)');
+  const results = regex.exec(location.search);
+  return results === null ? '' : decodeURIComponent(results[1].replace(/\+/g, ' '));
+}
diff --git a/superset/assets/javascripts/reduxUtils.js b/superset/assets/javascripts/reduxUtils.js
index fc42083..abe2d7f 100644
--- a/superset/assets/javascripts/reduxUtils.js
+++ b/superset/assets/javascripts/reduxUtils.js
@@ -19,10 +19,9 @@ export function alterInObject(state, arrKey, obj, alterations) {
   return Object.assign({}, state, { [arrKey]: newObject });
 }
 
-export function alterInArr(state, arrKey, obj, alterations) {
+export function alterInArr(state, arrKey, obj, alterations, idKey = 'id') {
   // Finds an item in an array in the state and replaces it with a
   // new object with an altered property
-  const idKey = 'id';
   const newArr = [];
   state[arrKey].forEach((arrItem) => {
     if (obj[idKey] === arrItem[idKey]) {
@@ -96,19 +95,5 @@ export function areArraysShallowEqual(arr1, arr2) {
 }
 
 export function areObjectsEqual(obj1, obj2) {
-  if (!obj1 || !obj2) {
-    return false;
-  }
-  if (!Object.keys(obj1).length !== Object.keys(obj2).length) {
-    return false;
-  }
-  for (const id in obj1) {
-    if (!obj2.hasOwnProperty(id)) {
-      return false;
-    }
-    if (obj1[id] !== obj2[id]) {
-      return false;
-    }
-  }
-  return true;
+  return JSON.stringify(obj1) === JSON.stringify(obj2);
 }
diff --git a/superset/assets/spec/javascripts/dashboard/SliceCell_spec.jsx b/superset/assets/spec/javascripts/dashboard/SliceCell_spec.jsx
deleted file mode 100644
index 8dbf661..0000000
--- a/superset/assets/spec/javascripts/dashboard/SliceCell_spec.jsx
+++ /dev/null
@@ -1,24 +0,0 @@
-import React from 'react';
-import { mount } from 'enzyme';
-import { describe, it } from 'mocha';
-import { expect } from 'chai';
-import { slice } from './fixtures';
-
-import SliceCell from '../../../javascripts/dashboard/components/SliceCell';
-
-describe('SliceCell', () => {
-  const mockedProps = {
-    slice,
-    removeSlice: () => {},
-    expandedSlices: {},
-  };
-  it('is valid', () => {
-    expect(
-      React.isValidElement(<SliceCell {...mockedProps} />),
-    ).to.equal(true);
-  });
-  it('renders six links', () => {
-    const wrapper = mount(<SliceCell {...mockedProps} />);
-    expect(wrapper.find('a')).to.have.length(6);
-  });
-});
diff --git a/superset/assets/spec/javascripts/dashboard/fixtures.jsx b/superset/assets/spec/javascripts/dashboard/fixtures.jsx
index 7c822d7..be515e3 100644
--- a/superset/assets/spec/javascripts/dashboard/fixtures.jsx
+++ b/superset/assets/spec/javascripts/dashboard/fixtures.jsx
@@ -66,5 +66,5 @@ export const contextData = {
   dash_save_perm: true,
   standalone_mode: false,
   dash_edit_perm: true,
-  user_id: '1',
+  userId: '1',
 };
diff --git a/superset/assets/spec/javascripts/explore/chartActions_spec.js b/superset/assets/spec/javascripts/explore/chartActions_spec.js
index b2e069a..f88de8f 100644
--- a/superset/assets/spec/javascripts/explore/chartActions_spec.js
+++ b/superset/assets/spec/javascripts/explore/chartActions_spec.js
@@ -3,7 +3,7 @@ import { expect } from 'chai';
 import sinon from 'sinon';
 import $ from 'jquery';
 import * as exploreUtils from '../../../javascripts/explore/exploreUtils';
-import * as actions from '../../../javascripts/explore/actions/chartActions';
+import * as actions from '../../../javascripts/chart/chartAction';
 
 describe('chart actions', () => {
   let dispatch;
diff --git a/superset/assets/spec/javascripts/explore/components/ChartContainer_spec.js b/superset/assets/spec/javascripts/explore/components/ExploreChartPanel_spec.js
similarity index 100%
rename from superset/assets/spec/javascripts/explore/components/ChartContainer_spec.js
rename to superset/assets/spec/javascripts/explore/components/ExploreChartPanel_spec.js
diff --git a/superset/assets/spec/javascripts/explore/components/SaveModal_spec.jsx b/superset/assets/spec/javascripts/explore/components/SaveModal_spec.jsx
index e548d21..346feda 100644
--- a/superset/assets/spec/javascripts/explore/components/SaveModal_spec.jsx
+++ b/superset/assets/spec/javascripts/explore/components/SaveModal_spec.jsx
@@ -24,7 +24,7 @@ describe('SaveModal', () => {
     },
     explore: {
       can_overwrite: true,
-      user_id: '1',
+      userId: '1',
       datasource: {},
       slice: {
         slice_id: 1,
diff --git a/superset/assets/spec/javascripts/explore/exploreActions_spec.js b/superset/assets/spec/javascripts/explore/exploreActions_spec.js
index 5d2926d..d37fc46 100644
--- a/superset/assets/spec/javascripts/explore/exploreActions_spec.js
+++ b/superset/assets/spec/javascripts/explore/exploreActions_spec.js
@@ -3,6 +3,7 @@ import { it, describe } from 'mocha';
 import { expect } from 'chai';
 import sinon from 'sinon';
 import $ from 'jquery';
+import * as chartActions from '../../../javascripts/chart/chartAction';
 import * as actions from '../../../javascripts/explore/actions/exploreActions';
 import { defaultState } from '../../../javascripts/explore/stores/store';
 import exploreReducer from '../../../javascripts/explore/reducers/exploreReducer';
@@ -77,7 +78,7 @@ describe('fetching actions', () => {
       ajaxStub.yieldsTo('success', { data: '' });
       makeRequest(true);
       expect(dispatch.callCount).to.equal(5);
-      expect(dispatch.getCall(4).args[0].type).to.equal(actions.TRIGGER_QUERY);
+      expect(dispatch.getCall(4).args[0].type).to.equal(chartActions.TRIGGER_QUERY);
     });
   });
 });
diff --git a/superset/assets/stylesheets/dashboard.css b/superset/assets/stylesheets/dashboard.css
index 289ead3..b110311 100644
--- a/superset/assets/stylesheets/dashboard.css
+++ b/superset/assets/stylesheets/dashboard.css
@@ -18,17 +18,26 @@ div.widget .chart-controls {
   right: 0;
   top: 5px;
   padding: 5px 5px;
+  opacity: 0;
+  transition: opacity 0.5s ease-in-out;
+}
+div.widget:hover .chart-controls {
   opacity: 0.75;
-  display: none;
+  transition: opacity 0.5s ease-in-out;
 }
 .slice-grid div.widget {
   border-radius: 0;
-  border: 0px;
+  border: 0;
   box-shadow: none;
   background-color: #fff;
   overflow: visible;
 }
 
+.slice-grid .slice_container {
+  background-color: #fff;
+  padding-left: 5px;
+}
+
 .dashboard .slice-grid .dragging,
 .dashboard .slice-grid .resizing {
   opacity: 0.5;
@@ -84,10 +93,12 @@ div.widget .chart-controls {
 .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"] {
@@ -95,7 +106,7 @@ div.widget .chart-controls {
 }
 
 .dashboard .separator.widget .slice_container {
-  padding: 0px;
+  padding: 0;
   overflow: visible;
 }
 .dashboard .separator.widget .slice_container hr {
@@ -116,6 +127,8 @@ div.widget .chart-controls {
 
 .dashboard .title .favstar {
   font-size: 20px;
+  position: relative;
+  top: -5px;
 }
 
 .chart-header .header {
diff --git a/superset/assets/stylesheets/superset.less b/superset/assets/stylesheets/superset.less
index ea43e54..a42c8ba 100644
--- a/superset/assets/stylesheets/superset.less
+++ b/superset/assets/stylesheets/superset.less
@@ -189,8 +189,23 @@ div.widget .chart-header a {
   display: none;
 }
 
-div.widget .slice_container {
-  overflow: hidden;
+div.widget {
+  .slice_container {
+    overflow: hidden;
+  }
+
+  .stack-trace-container.has-trace {
+    .alert-warning:hover {
+      cursor: pointer;
+    }
+  }
+
+  .is-loading {
+    .stack-trace-container,
+    .slice_container {
+      opacity: 0.5;
+    }
+  }
 }
 
 .navbar .alert {
diff --git a/superset/assets/visualizations/markup.js b/superset/assets/visualizations/markup.js
index 739e451..8b43716 100644
--- a/superset/assets/visualizations/markup.js
+++ b/superset/assets/visualizations/markup.js
@@ -22,7 +22,7 @@ function markupWidget(slice, payload) {
   jqdiv.html(`
     <iframe id="${iframeId}"
       frameborder="0"
-      height="${slice.height()}"
+      height="${slice.height() - 20}"
       sandbox="allow-same-origin allow-scripts allow-top-navigation allow-popups">
     </iframe>
   `);
diff --git a/superset/assets/webpack.config.js b/superset/assets/webpack.config.js
index bb1729c..ca1465e 100644
--- a/superset/assets/webpack.config.js
+++ b/superset/assets/webpack.config.js
@@ -19,7 +19,7 @@ const config = {
     common: APP_DIR + '/javascripts/common.js',
     addSlice: ['babel-polyfill', APP_DIR + '/javascripts/addSlice/index.jsx'],
     explore: ['babel-polyfill', APP_DIR + '/javascripts/explore/index.jsx'],
-    dashboard: ['babel-polyfill', APP_DIR + '/javascripts/dashboard/Dashboard.jsx'],
+    dashboard: ['babel-polyfill', APP_DIR + '/javascripts/dashboard/index.jsx'],
     sqllab: ['babel-polyfill', APP_DIR + '/javascripts/SqlLab/index.jsx'],
     welcome: ['babel-polyfill', APP_DIR + '/javascripts/welcome.js'],
     profile: ['babel-polyfill', APP_DIR + '/javascripts/profile/index.jsx'],
diff --git a/superset/templates/superset/dashboard.html b/superset/templates/superset/dashboard.html
index bb0b97c..1a158d9 100644
--- a/superset/templates/superset/dashboard.html
+++ b/superset/templates/superset/dashboard.html
@@ -6,11 +6,5 @@
   class="dashboard container-fluid"
   data-bootstrap="{{ bootstrap_data }}"
 >
-  <div id="alert-container"></div>
-  <div id="dashboard-header"></div>
-
-  <!-- gridster class used for backwards compatibility -->
-  <div id="grid-container" class="slice-grid gridster"></div>
-
 </div>
 {% endblock %}

-- 
To stop receiving notification emails like this one, please contact
['"commits@superset.apache.org" <commits@superset.apache.org>'].

Mime
View raw message