graceguo-supercat closed pull request #3993: [Explore view] Use POST method for 
charting requests
URL: https://github.com/apache/incubator-superset/pull/3993
 
 
   

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

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

diff --git a/superset/assets/javascripts/SqlLab/components/VisualizeModal.jsx 
b/superset/assets/javascripts/SqlLab/components/VisualizeModal.jsx
index 23692f9aa0..1c4315ff91 100644
--- a/superset/assets/javascripts/SqlLab/components/VisualizeModal.jsx
+++ b/superset/assets/javascripts/SqlLab/components/VisualizeModal.jsx
@@ -9,7 +9,7 @@ import { Alert, Button, Col, Modal } from 'react-bootstrap';
 import Select from 'react-select';
 import { Table } from 'reactable';
 import shortid from 'shortid';
-import { getExploreUrl } from '../../explore/exploreUtils';
+import { exportChart } from '../../explore/exploreUtils';
 import * as actions from '../actions';
 import { VISUALIZE_VALIDATION_ERRORS } from '../constants';
 import visTypes from '../../explore/stores/visTypes';
@@ -166,7 +166,8 @@ class VisualizeModal extends React.PureComponent {
         }
         notify.info(t('Creating a data source and popping a new tab'));
 
-        window.open(getExploreUrl(formData));
+        // open new window for data visualization
+        exportChart(formData);
       })
       .fail(() => {
         notify.error(this.props.errorMessage);
diff --git a/superset/assets/javascripts/chart/Chart.jsx 
b/superset/assets/javascripts/chart/Chart.jsx
index 7bb71aca47..cc18bb4fe6 100644
--- a/superset/assets/javascripts/chart/Chart.jsx
+++ b/superset/assets/javascripts/chart/Chart.jsx
@@ -188,6 +188,7 @@ class Chart extends React.PureComponent {
       });
       this.props.actions.chartRenderingSucceeded(this.props.chartKey);
     } catch (e) {
+      console.error(e);  // eslint-disable-line
       this.props.actions.chartRenderingFailed(e, this.props.chartKey);
     }
   }
diff --git a/superset/assets/javascripts/chart/chartAction.js 
b/superset/assets/javascripts/chart/chartAction.js
index 089a1dd1f7..5f80d9a01c 100644
--- a/superset/assets/javascripts/chart/chartAction.js
+++ b/superset/assets/javascripts/chart/chartAction.js
@@ -1,12 +1,12 @@
-import { getExploreUrl, getAnnotationJsonUrl } from '../explore/exploreUtils';
+import { getExploreUrlAndPayload, getAnnotationJsonUrl } from 
'../explore/exploreUtils';
 import { requiresQuery, ANNOTATION_SOURCE_TYPES } from 
'../modules/AnnotationTypes';
 import { Logger, LOG_ACTIONS_LOAD_EVENT } from '../logger';
 
 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 function chartUpdateStarted(queryRequest, latestQueryFormData, key) {
+  return { type: CHART_UPDATE_STARTED, queryRequest, latestQueryFormData, key 
};
 }
 
 export const CHART_UPDATE_SUCCEEDED = 'CHART_UPDATE_SUCCEEDED';
@@ -109,18 +109,22 @@ export function renderTriggered(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);
-    let logStart;
+    const { url, payload } = getExploreUrlAndPayload({
+      formData,
+      endpointType: 'json',
+      force,
+    });
+    const logStart = Logger.getTimestamp();
     const queryRequest = $.ajax({
+      type: 'POST',
       url,
       dataType: 'json',
-      timeout: timeout * 1000,
-      beforeSend: () => {
-        logStart = Logger.getTimestamp();
+      data: {
+        form_data: JSON.stringify(payload),
       },
+      timeout: timeout * 1000,
     });
-
-    const queryPromise = 
Promise.resolve(dispatch(chartUpdateStarted(queryRequest, key)))
+    const queryPromise = 
Promise.resolve(dispatch(chartUpdateStarted(queryRequest, payload, key)))
       .then(() => queryRequest)
       .then((queryResponse) => {
         Logger.append(LOG_ACTIONS_LOAD_EVENT, {
diff --git a/superset/assets/javascripts/chart/chartReducer.js 
b/superset/assets/javascripts/chart/chartReducer.js
index e1dfe0524d..6c4c43cc9c 100644
--- a/superset/assets/javascripts/chart/chartReducer.js
+++ b/superset/assets/javascripts/chart/chartReducer.js
@@ -24,7 +24,7 @@ export const chart = {
   chartStatus: 'loading',
   chartUpdateEndTime: null,
   chartUpdateStartTime: now(),
-  latestQueryFormData: null,
+  latestQueryFormData: {},
   queryRequest: null,
   queryResponse: null,
   triggerQuery: true,
@@ -47,6 +47,7 @@ export default function chartReducer(charts = {}, action) {
         chartUpdateEndTime: null,
         chartUpdateStartTime: now(),
         queryRequest: action.queryRequest,
+        latestQueryFormData: action.latestQueryFormData,
       };
     },
     [actions.CHART_UPDATE_STOPPED](state) {
diff --git a/superset/assets/javascripts/dashboard/actions.js 
b/superset/assets/javascripts/dashboard/actions.js
index 6da6b43ac7..c7f1a6aa26 100644
--- a/superset/assets/javascripts/dashboard/actions.js
+++ b/superset/assets/javascripts/dashboard/actions.js
@@ -1,6 +1,6 @@
 /* global notify */
 import $ from 'jquery';
-import { getExploreUrl } from '../explore/exploreUtils';
+import { getExploreUrlAndPayload } from '../explore/exploreUtils';
 
 export const ADD_FILTER = 'ADD_FILTER';
 export function addFilter(sliceId, col, vals, merge = true, refresh = true) {
@@ -59,10 +59,20 @@ export function saveSlice(slice, sliceName) {
     sliceParams.slice_id = slice.slice_id;
     sliceParams.action = 'overwrite';
     sliceParams.slice_name = sliceName;
-    const saveUrl = getExploreUrl(slice.form_data, 'base', false, null, 
sliceParams);
+
+    const { url, payload } = getExploreUrlAndPayload({
+      formData: slice.form_data,
+      endpointType: 'base',
+      force: false,
+      curUrl: null,
+      requestParams: sliceParams,
+    });
     return $.ajax({
-      url: saveUrl,
-      type: 'GET',
+      url,
+      type: 'POST',
+      data: {
+        form_data: JSON.stringify(payload),
+      },
       success: () => {
         dispatch(updateSliceName(slice, sliceName));
         notify.success('This slice name was saved successfully.');
diff --git a/superset/assets/javascripts/dashboard/components/Dashboard.jsx 
b/superset/assets/javascripts/dashboard/components/Dashboard.jsx
index bcc13778d2..2a6a227997 100644
--- a/superset/assets/javascripts/dashboard/components/Dashboard.jsx
+++ b/superset/assets/javascripts/dashboard/components/Dashboard.jsx
@@ -4,6 +4,7 @@ import PropTypes from 'prop-types';
 import AlertsWrapper from '../../components/AlertsWrapper';
 import GridLayout from './GridLayout';
 import Header from './Header';
+import { exportChart } from '../../explore/exploreUtils';
 import { areObjectsEqual } from '../../reduxUtils';
 import { Logger, ActionLog, LOG_ACTIONS_PAGE_LOAD,
   LOG_ACTIONS_LOAD_EVENT, LOG_ACTIONS_RENDER_EVENT } from '../../logger';
@@ -66,6 +67,8 @@ class Dashboard extends React.PureComponent {
     this.addSlicesToDashboard = this.addSlicesToDashboard.bind(this);
     this.fetchSlice = this.fetchSlice.bind(this);
     this.getFormDataExtra = this.getFormDataExtra.bind(this);
+    this.exploreChart = this.exploreChart.bind(this);
+    this.exportCSV = this.exportCSV.bind(this);
     this.props.actions.fetchFaveStar = 
this.props.actions.fetchFaveStar.bind(this);
     this.props.actions.saveFaveStar = 
this.props.actions.saveFaveStar.bind(this);
     this.props.actions.saveSlice = this.props.actions.saveSlice.bind(this);
@@ -274,6 +277,16 @@ class Dashboard extends React.PureComponent {
     });
   }
 
+  exploreChart(slice) {
+    const formData = this.getFormDataExtra(slice);
+    exportChart(formData);
+  }
+
+  exportCSV(slice) {
+    const formData = this.getFormDataExtra(slice);
+    exportChart(formData, 'csv');
+  }
+
   // re-render chart without fetch
   rerenderCharts() {
     this.getAllSlices().forEach((slice) => {
@@ -316,6 +329,8 @@ class Dashboard extends React.PureComponent {
             timeout={this.props.timeout}
             onChange={this.onChange}
             getFormDataExtra={this.getFormDataExtra}
+            exploreChart={this.exploreChart}
+            exportCSV={this.exportCSV}
             fetchSlice={this.fetchSlice}
             saveSlice={this.props.actions.saveSlice}
             removeSlice={this.props.actions.removeSlice}
diff --git a/superset/assets/javascripts/dashboard/components/GridCell.jsx 
b/superset/assets/javascripts/dashboard/components/GridCell.jsx
index 2748fccd9a..2ae9ea49c0 100644
--- a/superset/assets/javascripts/dashboard/components/GridCell.jsx
+++ b/superset/assets/javascripts/dashboard/components/GridCell.jsx
@@ -16,8 +16,6 @@ const propTypes = {
   isExpanded: PropTypes.bool,
   widgetHeight: PropTypes.number,
   widgetWidth: PropTypes.number,
-  exploreChartUrl: PropTypes.string,
-  exportCSVUrl: PropTypes.string,
   slice: PropTypes.object,
   chartKey: PropTypes.string,
   formData: PropTypes.object,
@@ -26,6 +24,8 @@ const propTypes = {
   removeSlice: PropTypes.func,
   updateSliceName: PropTypes.func,
   toggleExpandSlice: PropTypes.func,
+  exploreChart: PropTypes.func,
+  exportCSV: PropTypes.func,
   addFilter: PropTypes.func,
   getFilters: PropTypes.func,
   clearFilter: PropTypes.func,
@@ -39,6 +39,8 @@ const defaultProps = {
   removeSlice: () => ({}),
   updateSliceName: () => ({}),
   toggleExpandSlice: () => ({}),
+  exploreChart: () => ({}),
+  exportCSV: () => ({}),
   addFilter: () => ({}),
   getFilters: () => ({}),
   clearFilter: () => ({}),
@@ -83,9 +85,10 @@ class GridCell extends React.PureComponent {
 
   render() {
     const {
-      exploreChartUrl, exportCSVUrl, isExpanded, isLoading, isCached, 
cachedDttm,
+      isExpanded, isLoading, isCached, cachedDttm,
       removeSlice, updateSliceName, toggleExpandSlice, forceRefresh,
       chartKey, slice, datasource, formData, timeout, annotationQuery,
+      exploreChart, exportCSV,
     } = this.props;
     return (
       <div
@@ -95,8 +98,6 @@ class GridCell extends React.PureComponent {
         <div ref={this.getHeaderId(slice)}>
           <SliceHeader
             slice={slice}
-            exploreChartUrl={exploreChartUrl}
-            exportCSVUrl={exportCSVUrl}
             isExpanded={isExpanded}
             isCached={isCached}
             cachedDttm={cachedDttm}
@@ -106,6 +107,8 @@ class GridCell extends React.PureComponent {
             forceRefresh={forceRefresh}
             editMode={this.props.editMode}
             annotationQuery={annotationQuery}
+            exploreChart={exploreChart}
+            exportCSV={exportCSV}
           />
         </div>
         {
diff --git a/superset/assets/javascripts/dashboard/components/GridLayout.jsx 
b/superset/assets/javascripts/dashboard/components/GridLayout.jsx
index 68de7686ce..0361d652c5 100644
--- a/superset/assets/javascripts/dashboard/components/GridLayout.jsx
+++ b/superset/assets/javascripts/dashboard/components/GridLayout.jsx
@@ -3,7 +3,6 @@ import PropTypes from 'prop-types';
 import { Responsive, WidthProvider } from 'react-grid-layout';
 
 import GridCell from './GridCell';
-import { getExploreUrl } from '../../explore/exploreUtils';
 
 require('react-grid-layout/css/styles.css');
 require('react-resizable/css/styles.css');
@@ -18,6 +17,8 @@ const propTypes = {
   timeout: PropTypes.number,
   onChange: PropTypes.func,
   getFormDataExtra: PropTypes.func,
+  exploreChart: PropTypes.func,
+  exportCSV: PropTypes.func,
   fetchSlice: PropTypes.func,
   saveSlice: PropTypes.func,
   removeSlice: PropTypes.func,
@@ -34,6 +35,8 @@ const propTypes = {
 const defaultProps = {
   onChange: () => ({}),
   getFormDataExtra: () => ({}),
+  exploreChart: () => ({}),
+  exportCSV: () => ({}),
   fetchSlice: () => ({}),
   saveSlice: () => ({}),
   removeSlice: () => ({}),
@@ -149,8 +152,8 @@ class GridLayout extends React.Component {
             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')}
+            exploreChart={this.props.exploreChart}
+            exportCSV={this.props.exportCSV}
             isExpanded={!!this.isExpanded(slice)}
             isLoading={currentChart.chartStatus === 'loading'}
             isCached={queryResponse.is_cached}
diff --git a/superset/assets/javascripts/dashboard/components/SliceHeader.jsx 
b/superset/assets/javascripts/dashboard/components/SliceHeader.jsx
index 1f4b475d88..8abcc86d61 100644
--- a/superset/assets/javascripts/dashboard/components/SliceHeader.jsx
+++ b/superset/assets/javascripts/dashboard/components/SliceHeader.jsx
@@ -8,16 +8,15 @@ import TooltipWrapper from '../../components/TooltipWrapper';
 
 const propTypes = {
   slice: PropTypes.object.isRequired,
-  exploreChartUrl: PropTypes.string,
-  exportCSVUrl: PropTypes.string,
   isExpanded: PropTypes.bool,
   isCached: PropTypes.bool,
   cachedDttm: PropTypes.string,
-  formDataExtra: PropTypes.object,
   removeSlice: PropTypes.func,
   updateSliceName: PropTypes.func,
   toggleExpandSlice: PropTypes.func,
   forceRefresh: PropTypes.func,
+  exploreChart: PropTypes.func,
+  exportCSV: PropTypes.func,
   editMode: PropTypes.bool,
   annotationQuery: PropTypes.object,
   annotationError: PropTypes.object,
@@ -28,6 +27,8 @@ const defaultProps = {
   removeSlice: () => ({}),
   updateSliceName: () => ({}),
   toggleExpandSlice: () => ({}),
+  exploreChart: () => ({}),
+  exportCSV: () => ({}),
   editMode: false,
 };
 
@@ -36,6 +37,11 @@ class SliceHeader extends React.PureComponent {
     super(props);
 
     this.onSaveTitle = this.onSaveTitle.bind(this);
+    this.onToggleExpandSlice = this.onToggleExpandSlice.bind(this);
+    this.exportCSV = this.props.exportCSV.bind(this, this.props.slice);
+    this.exploreChart = this.props.exploreChart.bind(this, this.props.slice);
+    this.forceRefresh = this.props.forceRefresh.bind(this, 
this.props.slice.slice_id);
+    this.removeSlice = this.props.removeSlice.bind(this, this.props.slice);
   }
 
   onSaveTitle(newTitle) {
@@ -44,10 +50,13 @@ class SliceHeader extends React.PureComponent {
     }
   }
 
+  onToggleExpandSlice() {
+    this.props.toggleExpandSlice(this.props.slice, !this.props.isExpanded);
+  }
+
   render() {
     const slice = this.props.slice;
     const isCached = this.props.isCached;
-    const isExpanded = !!this.props.isExpanded;
     const cachedWhen = moment.utc(this.props.cachedDttm).fromNow();
     const refreshTooltip = isCached ?
       t('Served from data cached %s . Click to force refresh.', cachedWhen) :
@@ -97,10 +106,7 @@ class SliceHeader extends React.PureComponent {
                   </TooltipWrapper>
                 </a>
               }
-              <a
-                className={`refresh ${isCached ? 'danger' : ''}`}
-                onClick={() => (this.props.forceRefresh(slice.slice_id))}
-              >
+              <a className={`refresh ${isCached ? 'danger' : ''}`} 
onClick={this.forceRefresh}>
                 <TooltipWrapper
                   placement="top"
                   label="refresh"
@@ -110,7 +116,7 @@ class SliceHeader extends React.PureComponent {
                 </TooltipWrapper>
               </a>
               {slice.description &&
-              <a onClick={() => this.props.toggleExpandSlice(slice, 
!isExpanded)}>
+              <a onClick={this.onToggleExpandSlice}>
                 <TooltipWrapper
                   placement="top"
                   label="description"
@@ -129,7 +135,7 @@ class SliceHeader extends React.PureComponent {
                   <i className="fa fa-pencil" />
                 </TooltipWrapper>
               </a>
-              <a className="exportCSV" href={this.props.exportCSVUrl}>
+              <a className="exportCSV" onClick={this.exportCSV}>
                 <TooltipWrapper
                   placement="top"
                   label="exportCSV"
@@ -138,7 +144,7 @@ class SliceHeader extends React.PureComponent {
                   <i className="fa fa-table" />
                 </TooltipWrapper>
               </a>
-              <a className="exploreChart" href={this.props.exploreChartUrl} 
target="_blank">
+              <a className="exploreChart" onClick={this.exploreChart}>
                 <TooltipWrapper
                   placement="top"
                   label="exploreChart"
@@ -148,7 +154,7 @@ class SliceHeader extends React.PureComponent {
                 </TooltipWrapper>
               </a>
               {this.props.editMode &&
-                <a className="remove-chart" onClick={() => 
(this.props.removeSlice(slice))}>
+                <a className="remove-chart" onClick={this.removeSlice}>
                   <TooltipWrapper
                     placement="top"
                     label="close"
diff --git a/superset/assets/javascripts/explore/actions/exploreActions.js 
b/superset/assets/javascripts/explore/actions/exploreActions.js
index b5be4d351e..81c23e3baf 100644
--- a/superset/assets/javascripts/explore/actions/exploreActions.js
+++ b/superset/assets/javascripts/explore/actions/exploreActions.js
@@ -132,6 +132,11 @@ export function updateExploreEndpoints(jsonUrl, csvUrl, 
standaloneUrl) {
   return { type: UPDATE_EXPLORE_ENDPOINTS, jsonUrl, csvUrl, standaloneUrl };
 }
 
+export const SET_EXPLORE_CONTROLS = 'UPDATE_EXPLORE_CONTROLS';
+export function setExploreControls(formData) {
+  return { type: SET_EXPLORE_CONTROLS, formData };
+}
+
 export const REMOVE_CONTROL_PANEL_ALERT = 'REMOVE_CONTROL_PANEL_ALERT';
 export function removeControlPanelAlert() {
   return { type: REMOVE_CONTROL_PANEL_ALERT };
diff --git a/superset/assets/javascripts/explore/actions/saveModalActions.js 
b/superset/assets/javascripts/explore/actions/saveModalActions.js
index b1111287f2..096ff3bf1b 100644
--- a/superset/assets/javascripts/explore/actions/saveModalActions.js
+++ b/superset/assets/javascripts/explore/actions/saveModalActions.js
@@ -1,3 +1,5 @@
+import { getExploreUrlAndPayload } from '../exploreUtils';
+
 const $ = window.$ = require('jquery');
 
 export const FETCH_DASHBOARDS_SUCCEEDED = 'FETCH_DASHBOARDS_SUCCEEDED';
@@ -44,14 +46,27 @@ export function removeSaveModalAlert() {
   return { type: REMOVE_SAVE_MODAL_ALERT };
 }
 
-export function saveSlice(url) {
-  return function (dispatch) {
-    return $.get(url, (data, status) => {
-      if (status === 'success') {
+export function saveSlice(formData, requestParams) {
+  return (dispatch) => {
+    const { url, payload } = getExploreUrlAndPayload({
+      formData,
+      endpointType: 'base',
+      force: false,
+      curUrl: null,
+      requestParams,
+    });
+    return $.ajax({
+      type: 'POST',
+      url,
+      data: {
+        form_data: JSON.stringify(payload),
+      },
+      success: ((data) => {
         dispatch(saveSliceSuccess(data));
-      } else {
+      }),
+      error: (() => {
         dispatch(saveSliceFailed());
-      }
+      }),
     });
   };
 }
diff --git 
a/superset/assets/javascripts/explore/components/DisplayQueryButton.jsx 
b/superset/assets/javascripts/explore/components/DisplayQueryButton.jsx
index fe0cbdd42c..d098d2a9af 100644
--- a/superset/assets/javascripts/explore/components/DisplayQueryButton.jsx
+++ b/superset/assets/javascripts/explore/components/DisplayQueryButton.jsx
@@ -7,6 +7,7 @@ import sql from 'react-syntax-highlighter/dist/languages/sql';
 import json from 'react-syntax-highlighter/dist/languages/json';
 import github from 'react-syntax-highlighter/dist/styles/github';
 import CopyToClipboard from './../../components/CopyToClipboard';
+import { getExploreUrlAndPayload } from '../exploreUtils';
 
 import ModalTrigger from './../../components/ModalTrigger';
 import Button from '../../components/Button';
@@ -23,7 +24,7 @@ const propTypes = {
   animation: PropTypes.bool,
   queryResponse: PropTypes.object,
   chartStatus: PropTypes.string,
-  queryEndpoint: PropTypes.string.isRequired,
+  latestQueryFormData: PropTypes.object.isRequired,
 };
 const defaultProps = {
   animation: true,
@@ -51,9 +52,16 @@ export default class DisplayQueryButton extends 
React.PureComponent {
   }
   fetchQuery() {
     this.setState({ isLoading: true });
+    const { url, payload } = getExploreUrlAndPayload({
+      formData: this.props.latestQueryFormData,
+      endpointType: 'query',
+    });
     $.ajax({
-      type: 'GET',
-      url: this.props.queryEndpoint,
+      type: 'POST',
+      url,
+      data: {
+        form_data: JSON.stringify(payload),
+      },
       success: (data) => {
         this.setState({
           language: data.language,
diff --git a/superset/assets/javascripts/explore/components/EmbedCodeButton.jsx 
b/superset/assets/javascripts/explore/components/EmbedCodeButton.jsx
index 01d59e1e2a..a09a5335ad 100644
--- a/superset/assets/javascripts/explore/components/EmbedCodeButton.jsx
+++ b/superset/assets/javascripts/explore/components/EmbedCodeButton.jsx
@@ -2,10 +2,11 @@ import React from 'react';
 import PropTypes from 'prop-types';
 import { Popover, OverlayTrigger } from 'react-bootstrap';
 import CopyToClipboard from './../../components/CopyToClipboard';
+import { getExploreLongUrl } from '../exploreUtils';
 import { t } from '../../locales';
 
 const propTypes = {
-  slice: PropTypes.object.isRequired,
+  latestQueryFormData: PropTypes.object.isRequired,
 };
 
 export default class EmbedCodeButton extends React.Component {
@@ -29,7 +30,7 @@ export default class EmbedCodeButton extends React.Component {
   generateEmbedHTML() {
     const srcLink = (
       window.location.origin +
-      this.props.slice.data.standalone_endpoint +
+      getExploreLongUrl(this.props.latestQueryFormData, 'standalone') +
       `&height=${this.state.height}`
     );
     return (
diff --git 
a/superset/assets/javascripts/explore/components/ExploreActionButtons.jsx 
b/superset/assets/javascripts/explore/components/ExploreActionButtons.jsx
index 57f2dfd744..74f9b73348 100644
--- a/superset/assets/javascripts/explore/components/ExploreActionButtons.jsx
+++ b/superset/assets/javascripts/explore/components/ExploreActionButtons.jsx
@@ -5,29 +5,33 @@ import URLShortLinkButton from './URLShortLinkButton';
 import EmbedCodeButton from './EmbedCodeButton';
 import DisplayQueryButton from './DisplayQueryButton';
 import { t } from '../../locales';
+import { exportChart } from '../exploreUtils';
 
 const propTypes = {
   canDownload: PropTypes.oneOfType([PropTypes.string, 
PropTypes.bool]).isRequired,
   slice: PropTypes.object,
-  queryEndpoint: PropTypes.string.isRequired,
-  queryResponse: PropTypes.object,
   chartStatus: PropTypes.string,
+  latestQueryFormData: PropTypes.object,
+  queryResponse: PropTypes.object,
 };
 
 export default function ExploreActionButtons({
-    chartStatus, canDownload, slice, queryResponse, queryEndpoint }) {
+    canDownload, slice, chartStatus, latestQueryFormData, queryResponse }) {
   const exportToCSVClasses = cx('btn btn-default btn-sm', {
     'disabled disabledButton': !canDownload,
   });
+  const doExportCSV = exportChart.bind(this, latestQueryFormData, 'csv');
+  const doExportChart = exportChart.bind(this, latestQueryFormData, 'json');
+
   if (slice) {
     return (
       <div className="btn-group results" role="group">
-        <URLShortLinkButton slice={slice} />
+        <URLShortLinkButton latestQueryFormData={latestQueryFormData} />
 
-        <EmbedCodeButton slice={slice} />
+        <EmbedCodeButton latestQueryFormData={latestQueryFormData} />
 
         <a
-          href={slice.data.json_endpoint}
+          onClick={doExportChart}
           className="btn btn-default btn-sm"
           title={t('Export to .json')}
           target="_blank"
@@ -37,7 +41,7 @@ export default function ExploreActionButtons({
         </a>
 
         <a
-          href={slice.data.csv_endpoint}
+          onClick={doExportCSV}
           className={exportToCSVClasses}
           title={t('Export to .csv format')}
           target="_blank"
@@ -48,14 +52,14 @@ export default function ExploreActionButtons({
 
         <DisplayQueryButton
           queryResponse={queryResponse}
-          queryEndpoint={queryEndpoint}
+          latestQueryFormData={latestQueryFormData}
           chartStatus={chartStatus}
         />
       </div>
     );
   }
   return (
-    <DisplayQueryButton queryEndpoint={queryEndpoint} />
+    <DisplayQueryButton latestQueryFormData={latestQueryFormData} />
   );
 }
 
diff --git 
a/superset/assets/javascripts/explore/components/ExploreChartHeader.jsx 
b/superset/assets/javascripts/explore/components/ExploreChartHeader.jsx
index 0faf164df4..00231df4a2 100644
--- a/superset/assets/javascripts/explore/components/ExploreChartHeader.jsx
+++ b/superset/assets/javascripts/explore/components/ExploreChartHeader.jsx
@@ -9,7 +9,6 @@ import AlteredSliceTag from '../../components/AlteredSliceTag';
 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';
 
@@ -21,6 +20,7 @@ const CHART_STATUS_MAP = {
 
 const propTypes = {
   actions: PropTypes.object.isRequired,
+  addHistory: PropTypes.func,
   can_overwrite: PropTypes.bool.isRequired,
   can_download: PropTypes.bool.isRequired,
   isStarred: PropTypes.bool.isRequired,
@@ -43,13 +43,13 @@ class ExploreChartHeader extends React.PureComponent {
       slice_name: newTitle,
       action: isNewSlice ? 'saveas' : 'overwrite',
     };
-    const saveUrl = getExploreUrl(this.props.form_data, 'base', false, null, 
params);
-    this.props.actions.saveSlice(saveUrl)
+    this.props.actions.saveSlice(this.props.form_data, params)
       .then((data) => {
         if (isNewSlice) {
           this.props.actions.createNewSlice(
             data.can_add, data.can_download, data.can_overwrite,
             data.slice, data.form_data);
+          this.props.addHistory({ isReplace: true, title: `[slice] 
${data.slice.slice_name}` });
         } else {
           this.props.actions.updateChartTitle(newTitle);
         }
@@ -68,12 +68,12 @@ class ExploreChartHeader extends React.PureComponent {
 
   render() {
     const formData = this.props.form_data;
-    const queryResponse = this.props.chart.queryResponse;
-    const data = {
-      csv_endpoint: getExploreUrl(formData, 'csv'),
-      json_endpoint: getExploreUrl(formData, 'json'),
-      standalone_endpoint: getExploreUrl(formData, 'standalone'),
-    };
+    const {
+      chartStatus,
+      chartUpdateEndTime,
+      chartUpdateStartTime,
+      latestQueryFormData,
+      queryResponse } = this.props.chart;
     const chartSucceeded = ['success', 
'rendered'].indexOf(this.props.chart.chartStatus) > 0;
     return (
       <div
@@ -126,18 +126,18 @@ class ExploreChartHeader extends React.PureComponent {
               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]}
+            startTime={chartUpdateStartTime}
+            endTime={chartUpdateEndTime}
+            isRunning={chartStatus === 'loading'}
+            status={CHART_STATUS_MAP[chartStatus]}
             style={{ fontSize: '10px', marginRight: '5px' }}
           />
           <ExploreActionButtons
-            slice={Object.assign({}, this.props.slice, { data })}
+            slice={this.props.slice}
             canDownload={this.props.can_download}
-            chartStatus={this.props.chart.chartStatus}
+            chartStatus={chartStatus}
+            latestQueryFormData={latestQueryFormData}
             queryResponse={queryResponse}
-            queryEndpoint={getExploreUrl(formData, 'query')}
           />
         </div>
       </div>
diff --git 
a/superset/assets/javascripts/explore/components/ExploreChartPanel.jsx 
b/superset/assets/javascripts/explore/components/ExploreChartPanel.jsx
index abb1bc0284..5e4926ed26 100644
--- a/superset/assets/javascripts/explore/components/ExploreChartPanel.jsx
+++ b/superset/assets/javascripts/explore/components/ExploreChartPanel.jsx
@@ -9,6 +9,7 @@ import ExploreChartHeader from './ExploreChartHeader';
 
 const propTypes = {
   actions: PropTypes.object.isRequired,
+  addHistory: PropTypes.func,
   can_overwrite: PropTypes.bool.isRequired,
   can_download: PropTypes.bool.isRequired,
   datasource: PropTypes.object,
@@ -58,6 +59,7 @@ class ExploreChartPanel extends React.PureComponent {
     const header = (
       <ExploreChartHeader
         actions={this.props.actions}
+        addHistory={this.props.addHistory}
         can_overwrite={this.props.can_overwrite}
         can_download={this.props.can_download}
         isStarred={this.props.isStarred}
diff --git 
a/superset/assets/javascripts/explore/components/ExploreViewContainer.jsx 
b/superset/assets/javascripts/explore/components/ExploreViewContainer.jsx
index 9b1ec1629b..d721e96f69 100644
--- a/superset/assets/javascripts/explore/components/ExploreViewContainer.jsx
+++ b/superset/assets/javascripts/explore/components/ExploreViewContainer.jsx
@@ -8,7 +8,7 @@ import ExploreChartPanel from './ExploreChartPanel';
 import ControlPanelsContainer from './ControlPanelsContainer';
 import SaveModal from './SaveModal';
 import QueryAndSaveBtns from './QueryAndSaveBtns';
-import { getExploreUrl } from '../exploreUtils';
+import { getExploreUrlAndPayload, getExploreLongUrl } from '../exploreUtils';
 import { areObjectsEqual } from '../../reduxUtils';
 import { getFormDataFromControls } from '../stores/store';
 import { chartPropType } from '../../chart/chartReducer';
@@ -50,10 +50,16 @@ class ExploreViewContainer extends React.Component {
       width: this.getWidth(),
       showModal: false,
     };
+
+    this.addHistory = this.addHistory.bind(this);
+    this.handleResize = this.handleResize.bind(this);
+    this.handlePopstate = this.handlePopstate.bind(this);
   }
 
   componentDidMount() {
-    window.addEventListener('resize', this.handleResize.bind(this));
+    window.addEventListener('resize', this.handleResize);
+    window.addEventListener('popstate', this.handlePopstate);
+    this.addHistory({ isReplace: true });
   }
 
   componentWillReceiveProps(np) {
@@ -72,7 +78,8 @@ class ExploreViewContainer extends React.Component {
     // if any control value changed and it's an instant control
     if (Object.keys(np.controls).some(key => (np.controls[key].renderTrigger &&
       typeof this.props.controls[key] !== 'undefined' &&
-      !areObjectsEqual(np.controls[key].value, 
this.props.controls[key].value)))) {
+      !areObjectsEqual(np.controls[key].value, 
this.props.controls[key].value)))
+    ) {
       this.props.actions.renderTriggered(new Date().getTime(), 
this.props.chart.chartKey);
     }
   }
@@ -82,7 +89,8 @@ class ExploreViewContainer extends React.Component {
   }
 
   componentWillUnmount() {
-    window.removeEventListener('resize', this.handleResize.bind(this));
+    window.removeEventListener('resize', this.handleResize);
+    window.removeEventListener('popstate', this.handlePopstate);
   }
 
   onQuery() {
@@ -90,10 +98,7 @@ class ExploreViewContainer extends React.Component {
     this.props.actions.removeControlPanelAlert();
     this.props.actions.triggerQuery(true, this.props.chart.chartKey);
 
-    history.pushState(
-      {},
-      document.title,
-      getExploreUrl(this.props.form_data));
+    this.addHistory({});
   }
 
   onStop() {
@@ -119,6 +124,27 @@ class ExploreViewContainer extends React.Component {
     }
   }
 
+  addHistory({ isReplace = false, title }) {
+    const { payload } = getExploreUrlAndPayload({ formData: 
this.props.form_data });
+    const longUrl = getExploreLongUrl(this.props.form_data);
+    if (isReplace) {
+      history.replaceState(
+        payload,
+        title,
+        longUrl);
+    } else {
+      history.pushState(
+        payload,
+        title,
+        longUrl);
+    }
+
+    // it seems some browsers don't support pushState title attribute
+    if (title) {
+      document.title = title;
+    }
+  }
+
   handleResize() {
     clearTimeout(this.resizeTimer);
     this.resizeTimer = setTimeout(() => {
@@ -126,6 +152,19 @@ class ExploreViewContainer extends React.Component {
     }, 250);
   }
 
+  handlePopstate() {
+    const formData = history.state;
+    if (formData && Object.keys(formData).length) {
+      this.props.actions.setExploreControls(formData);
+      this.props.actions.runQuery(
+        formData,
+        false,
+        this.props.timeout,
+        this.props.chart.chartKey,
+      );
+    }
+  }
+
   toggleModal() {
     this.setState({ showModal: !this.state.showModal });
   }
@@ -162,6 +201,7 @@ class ExploreViewContainer extends React.Component {
         width={this.state.width}
         height={this.state.height}
         {...this.props}
+        addHistory={this.addHistory}
       />);
   }
 
diff --git a/superset/assets/javascripts/explore/components/SaveModal.jsx 
b/superset/assets/javascripts/explore/components/SaveModal.jsx
index 2d716ae93b..6cc1a8ca2b 100644
--- a/superset/assets/javascripts/explore/components/SaveModal.jsx
+++ b/superset/assets/javascripts/explore/components/SaveModal.jsx
@@ -5,7 +5,6 @@ import { connect } from 'react-redux';
 
 import { Modal, Alert, Button, Radio } from 'react-bootstrap';
 import Select from 'react-select';
-import { getExploreUrl } from '../exploreUtils';
 import { t } from '../../locales';
 
 const propTypes = {
@@ -104,8 +103,7 @@ class SaveModal extends React.Component {
     }
     sliceParams.goto_dash = gotodash;
 
-    const saveUrl = getExploreUrl(this.props.form_data, 'base', false, null, 
sliceParams);
-    this.props.actions.saveSlice(saveUrl)
+    this.props.actions.saveSlice(this.props.form_data, sliceParams)
       .then((data) => {
         // Go to new slice url or dashboard url
         if (gotodash) {
diff --git 
a/superset/assets/javascripts/explore/components/URLShortLinkButton.jsx 
b/superset/assets/javascripts/explore/components/URLShortLinkButton.jsx
index ddae9a2206..aa278b7a46 100644
--- a/superset/assets/javascripts/explore/components/URLShortLinkButton.jsx
+++ b/superset/assets/javascripts/explore/components/URLShortLinkButton.jsx
@@ -3,10 +3,11 @@ import PropTypes from 'prop-types';
 import { Popover, OverlayTrigger } from 'react-bootstrap';
 import CopyToClipboard from './../../components/CopyToClipboard';
 import { getShortUrl } from '../../../utils/common';
+import { getExploreLongUrl } from '../exploreUtils';
 import { t } from '../../locales';
 
 const propTypes = {
-  slice: PropTypes.object.isRequired,
+  latestQueryFormData: PropTypes.object.isRequired,
 };
 
 export default class URLShortLinkButton extends React.Component {
@@ -24,7 +25,7 @@ export default class URLShortLinkButton extends 
React.Component {
   }
 
   getCopyUrl() {
-    const longUrl = window.location.pathname + window.location.search;
+    const longUrl = getExploreLongUrl(this.props.latestQueryFormData);
     getShortUrl(longUrl, this.onShortUrlSuccess.bind(this));
   }
 
diff --git 
a/superset/assets/javascripts/explore/components/controls/TextControl.jsx 
b/superset/assets/javascripts/explore/components/controls/TextControl.jsx
index bfe3f99177..ed1238e509 100644
--- a/superset/assets/javascripts/explore/components/controls/TextControl.jsx
+++ b/superset/assets/javascripts/explore/components/controls/TextControl.jsx
@@ -13,6 +13,7 @@ const propTypes = {
   ]),
   isFloat: PropTypes.bool,
   isInt: PropTypes.bool,
+  disabled: PropTypes.bool,
 };
 
 const defaultProps = {
@@ -21,6 +22,7 @@ const defaultProps = {
   value: '',
   isInt: false,
   isFloat: false,
+  disabled: false,
 };
 
 export default class TextControl extends React.Component {
@@ -63,6 +65,7 @@ export default class TextControl extends React.Component {
             onChange={this.onChange}
             onFocus={this.props.onFocus}
             value={value}
+            disabled={this.props.disabled}
           />
         </FormGroup>
       </div>
diff --git a/superset/assets/javascripts/explore/exploreUtils.js 
b/superset/assets/javascripts/explore/exploreUtils.js
index 8a01745d9a..309fce1e8e 100644
--- a/superset/assets/javascripts/explore/exploreUtils.js
+++ b/superset/assets/javascripts/explore/exploreUtils.js
@@ -19,31 +19,59 @@ export function getAnnotationJsonUrl(slice_id, form_data, 
isNative) {
     }).toString();
 }
 
-export function getExploreUrl(form_data, endpointType = 'base', force = false,
-  curUrl = null, requestParams = {}) {
-  if (!form_data.datasource) {
+export function getURIDirectory(formData, endpointType = 'base') {
+  // Building the directory part of the URI
+  let directory = '/superset/explore/';
+  if (['json', 'csv', 'query'].indexOf(endpointType) >= 0) {
+    directory = '/superset/explore_json/';
+  }
+  const [datasource_id, datasource_type] = formData.datasource.split('__');
+  directory += `${datasource_type}/${datasource_id}/`;
+
+  return directory;
+}
+
+export function getExploreLongUrl(formData, endpointType) {
+  if (!formData.datasource) {
+    return null;
+  }
+
+  const uri = new URI('/');
+  const directory = getURIDirectory(formData, endpointType);
+  const search = uri.search(true);
+  search.form_data = JSON.stringify(formData);
+  if (endpointType === 'standalone') {
+    search.standalone = 'true';
+  }
+  return uri.directory(directory).search(search).toString();
+}
+
+export function getExploreUrlAndPayload({
+  formData,
+  endpointType = 'base',
+  force = false,
+  curUrl = null,
+  requestParams = {},
+}) {
+  if (!formData.datasource) {
     return null;
   }
 
   // The search params from the window.location are carried through,
   // but can be specified with curUrl (used for unit tests to spoof
   // the window.location).
-  let uri = URI(window.location.search);
+  let uri = new URI([location.protocol, '//', location.host].join(''));
   if (curUrl) {
     uri = URI(URI(curUrl).search());
   }
 
-  // Building the directory part of the URI
-  let directory = '/superset/explore/';
-  if (['json', 'csv', 'query'].indexOf(endpointType) >= 0) {
-    directory = '/superset/explore_json/';
-  }
-  const [datasource_id, datasource_type] = form_data.datasource.split('__');
-  directory += `${datasource_type}/${datasource_id}/`;
+  const directory = getURIDirectory(formData, endpointType);
 
   // Building the querystring (search) part of the URI
   const search = uri.search(true);
-  search.form_data = JSON.stringify(form_data);
+  if (formData.slice_id) {
+    search.form_data = JSON.stringify({ slice_id: formData.slice_id });
+  }
   if (force) {
     search.force = 'true';
   }
@@ -65,5 +93,33 @@ export function getExploreUrl(form_data, endpointType = 
'base', force = false,
     });
   }
   uri = uri.search(search).directory(directory);
-  return uri.toString();
+  const payload = { ...formData };
+
+  return {
+    url: uri.toString(),
+    payload,
+  };
+}
+
+export function exportChart(formData, endpointType) {
+  const { url, payload } = getExploreUrlAndPayload({ formData, endpointType });
+
+  const exploreForm = document.createElement('form');
+  exploreForm.action = url;
+  exploreForm.method = 'POST';
+  exploreForm.target = '_blank';
+  const token = document.createElement('input');
+  token.type = 'hidden';
+  token.name = 'csrf_token';
+  token.value = (document.getElementById('csrf_token') || {}).value;
+  exploreForm.appendChild(token);
+  const data = document.createElement('input');
+  data.type = 'hidden';
+  data.name = 'form_data';
+  data.value = JSON.stringify(payload);
+  exploreForm.appendChild(data);
+
+  document.body.appendChild(exploreForm);
+  exploreForm.submit();
+  document.body.removeChild(exploreForm);
 }
diff --git a/superset/assets/javascripts/explore/reducers/exploreReducer.js 
b/superset/assets/javascripts/explore/reducers/exploreReducer.js
index 7b55748800..e366f285f4 100644
--- a/superset/assets/javascripts/explore/reducers/exploreReducer.js
+++ b/superset/assets/javascripts/explore/reducers/exploreReducer.js
@@ -56,6 +56,10 @@ export default function exploreReducer(state = {}, action) {
       }
       return Object.assign({}, state, changes);
     },
+    [actions.SET_EXPLORE_CONTROLS]() {
+      const controls = getControlsState(state, action.formData);
+      return Object.assign({}, state, { controls });
+    },
     [actions.UPDATE_CHART_TITLE]() {
       const updatedSlice = Object.assign({}, state.slice, { slice_name: 
action.slice_name });
       return Object.assign({}, state, { slice: updatedSlice });
diff --git a/superset/assets/javascripts/explore/stores/controls.jsx 
b/superset/assets/javascripts/explore/stores/controls.jsx
index 8e3420bd81..007523c4d1 100644
--- a/superset/assets/javascripts/explore/stores/controls.jsx
+++ b/superset/assets/javascripts/explore/stores/controls.jsx
@@ -331,6 +331,14 @@ export const controls = {
     default: false,
   },
 
+  autozoom: {
+    type: 'CheckboxControl',
+    label: t('Auto Zoom'),
+    default: true,
+    renderTrigger: true,
+    description: t('When checked, the map will zoom to your data after each 
query'),
+  },
+
   show_perc: {
     type: 'CheckboxControl',
     label: t('Show percentage'),
@@ -862,8 +870,11 @@ export const controls = {
     label: t('Series limit'),
     validators: [v.integer],
     choices: formatSelectOptions(SERIES_LIMITS),
-    default: 50,
-    description: t('Limits the number of time series that get displayed'),
+    description: t(
+      'Limits the number of time series that get displayed. A sub query ' +
+      '(or an extra phase where sub queries are not supported) is applied to 
limit ' +
+      'the number of time series that get fetched and displayed. This feature 
is useful ' +
+      'when grouping by high cardinality dimension(s).'),
   },
 
   timeseries_limit_metric: {
diff --git a/superset/assets/javascripts/explore/stores/visTypes.js 
b/superset/assets/javascripts/explore/stores/visTypes.js
index 9d62eada88..3135b2613d 100644
--- a/superset/assets/javascripts/explore/stores/visTypes.js
+++ b/superset/assets/javascripts/explore/stores/visTypes.js
@@ -369,7 +369,7 @@ export const visTypes = {
         label: t('Map'),
         controlSetRows: [
           ['mapbox_style', 'viewport'],
-          ['color_picker', null],
+          ['color_picker', 'autozoom'],
           ['grid_size', 'extruded'],
         ],
       },
@@ -407,7 +407,7 @@ export const visTypes = {
         label: t('Map'),
         controlSetRows: [
           ['mapbox_style', 'viewport'],
-          ['color_picker', null],
+          ['color_picker', 'autozoom'],
           ['grid_size', 'extruded'],
         ],
       },
@@ -448,7 +448,7 @@ export const visTypes = {
         controlSetRows: [
           ['mapbox_style', 'viewport'],
           ['color_picker', 'line_width'],
-          ['reverse_long_lat', null],
+          ['reverse_long_lat', 'autozoom'],
         ],
       },
       {
@@ -479,6 +479,7 @@ export const visTypes = {
         label: t('Map'),
         controlSetRows: [
           ['mapbox_style', 'viewport'],
+          ['autozoom', null],
         ],
       },
       {
@@ -521,6 +522,7 @@ export const visTypes = {
         label: t('Map'),
         controlSetRows: [
           ['mapbox_style', 'viewport'],
+          // TODO ['autozoom', null],
         ],
       },
       {
@@ -600,6 +602,7 @@ export const visTypes = {
         label: t('Map'),
         controlSetRows: [
           ['mapbox_style', 'viewport'],
+          ['autozoom', null],
         ],
       },
       {
@@ -635,8 +638,10 @@ export const visTypes = {
       },
       {
         label: t('Map'),
+        expanded: true,
         controlSetRows: [
           ['mapbox_style', 'viewport'],
+          ['autozoom', null],
         ],
       },
       {
diff --git a/superset/assets/package.json b/superset/assets/package.json
index abc978c079..fbfa32a27d 100644
--- a/superset/assets/package.json
+++ b/superset/assets/package.json
@@ -60,6 +60,7 @@
     "distributions": "^1.0.0",
     "dompurify": "^1.0.3",
     "fastdom": "^1.0.6",
+    "geojson-extent": "^0.3.2",
     "geolib": "^2.0.24",
     "immutable": "^3.8.2",
     "jed": "^1.1.1",
@@ -105,7 +106,7 @@
     "supercluster": 
"https://github.com/georgeke/supercluster/tarball/ac3492737e7ce98e07af679623aad452373bbc40";,
     "underscore": "^1.8.3",
     "urijs": "^1.18.10",
-    "viewport-mercator-project": "^2.1.0"
+    "viewport-mercator-project": "^5.0.0"
   },
   "devDependencies": {
     "babel-cli": "^6.14.0",
@@ -137,6 +138,7 @@
     "less": "^2.6.1",
     "less-loader": "^4.0.3",
     "mocha": "^3.2.0",
+    "npm-check-updates": "^2.14.0",
     "react-addons-test-utils": "^15.6.2",
     "react-test-renderer": "^15.6.2",
     "redux-mock-store": "^1.2.3",
diff --git a/superset/assets/spec/javascripts/explore/chartActions_spec.js 
b/superset/assets/spec/javascripts/explore/chartActions_spec.js
index 4caeccd3e2..d0f6c6b3d4 100644
--- a/superset/assets/spec/javascripts/explore/chartActions_spec.js
+++ b/superset/assets/spec/javascripts/explore/chartActions_spec.js
@@ -13,7 +13,8 @@ describe('chart actions', () => {
 
   beforeEach(() => {
     dispatch = sinon.spy();
-    urlStub = sinon.stub(exploreUtils, 'getExploreUrl').callsFake(() => 
('mockURL'));
+    urlStub = sinon.stub(exploreUtils, 'getExploreUrlAndPayload')
+      .callsFake(() => ({ url: 'mockURL', payload: {} }));
     ajaxStub = sinon.stub($, 'ajax');
   });
 
diff --git 
a/superset/assets/spec/javascripts/explore/components/EmbedCodeButton_spec.jsx 
b/superset/assets/spec/javascripts/explore/components/EmbedCodeButton_spec.jsx
index 837269b935..f60b254ee0 100644
--- 
a/superset/assets/spec/javascripts/explore/components/EmbedCodeButton_spec.jsx
+++ 
b/superset/assets/spec/javascripts/explore/components/EmbedCodeButton_spec.jsx
@@ -3,16 +3,14 @@ import { expect } from 'chai';
 import { describe, it } from 'mocha';
 import { shallow, mount } from 'enzyme';
 import { OverlayTrigger } from 'react-bootstrap';
+import sinon from 'sinon';
 
 import EmbedCodeButton from 
'../../../../javascripts/explore/components/EmbedCodeButton';
+import * as exploreUtils from '../../../../javascripts/explore/exploreUtils';
 
 describe('EmbedCodeButton', () => {
   const defaultProps = {
-    slice: {
-      data: {
-        standalone_endpoint: 'endpoint_url',
-      },
-    },
+    latestQueryFormData: { datasource: '107__table' },
   };
 
   it('renders', () => {
@@ -25,11 +23,11 @@ describe('EmbedCodeButton', () => {
   });
 
   it('returns correct embed code', () => {
+    const stub = sinon.stub(exploreUtils, 'getExploreLongUrl').callsFake(() => 
('endpoint_url'));
     const wrapper = mount(<EmbedCodeButton {...defaultProps} />);
     wrapper.setState({
       height: '1000',
       width: '2000',
-      srcLink: 'http://localhost/endpoint_url',
     });
     const embedHTML = (
       '<iframe\n' +
@@ -43,5 +41,6 @@ describe('EmbedCodeButton', () => {
       '</iframe>'
     );
     expect(wrapper.instance().generateEmbedHTML()).to.equal(embedHTML);
+    stub.restore();
   });
 });
diff --git 
a/superset/assets/spec/javascripts/explore/components/SaveModal_spec.jsx 
b/superset/assets/spec/javascripts/explore/components/SaveModal_spec.jsx
index e548d21a60..00d040aa1e 100644
--- a/superset/assets/spec/javascripts/explore/components/SaveModal_spec.jsx
+++ b/superset/assets/spec/javascripts/explore/components/SaveModal_spec.jsx
@@ -38,7 +38,7 @@ describe('SaveModal', () => {
   const defaultProps = {
     onHide: () => ({}),
     actions: saveModalActions,
-    form_data: {},
+    form_data: { datasource: '107__table' },
   };
   const mockEvent = {
     target: {
@@ -117,7 +117,7 @@ describe('SaveModal', () => {
 
   describe('saveOrOverwrite', () => {
     beforeEach(() => {
-      sinon.stub(exploreUtils, 'getExploreUrl').callsFake(() => ('mockURL'));
+      sinon.stub(exploreUtils, 'getExploreUrlAndPayload').callsFake(() => ({ 
url: 'mockURL', payload: defaultProps.form_data }));
       sinon.stub(saveModalActions, 'saveSlice').callsFake(() => {
         const d = $.Deferred();
         d.resolve('done');
@@ -125,14 +125,15 @@ describe('SaveModal', () => {
       });
     });
     afterEach(() => {
-      exploreUtils.getExploreUrl.restore();
+      exploreUtils.getExploreUrlAndPayload.restore();
       saveModalActions.saveSlice.restore();
     });
 
     it('should save slice', () => {
       const wrapper = getWrapper();
       wrapper.instance().saveOrOverwrite(true);
-      
expect(saveModalActions.saveSlice.getCall(0).args[0]).to.equal('mockURL');
+      const args = saveModalActions.saveSlice.getCall(0).args;
+      expect(args[0]).to.deep.equal(defaultProps.form_data);
     });
     it('existing dashboard', () => {
       const wrapper = getWrapper();
@@ -144,8 +145,8 @@ describe('SaveModal', () => {
 
       wrapper.setState({ saveToDashboardId });
       wrapper.instance().saveOrOverwrite(true);
-      const args = exploreUtils.getExploreUrl.getCall(0).args;
-      expect(args[4].save_to_dashboard_id).to.equal(saveToDashboardId);
+      const args = saveModalActions.saveSlice.getCall(0).args;
+      expect(args[1].save_to_dashboard_id).to.equal(saveToDashboardId);
     });
     it('new dashboard', () => {
       const wrapper = getWrapper();
@@ -157,8 +158,8 @@ describe('SaveModal', () => {
 
       wrapper.setState({ newDashboardName });
       wrapper.instance().saveOrOverwrite(true);
-      const args = exploreUtils.getExploreUrl.getCall(0).args;
-      expect(args[4].new_dashboard_name).to.equal(newDashboardName);
+      const args = saveModalActions.saveSlice.getCall(0).args;
+      expect(args[1].new_dashboard_name).to.equal(newDashboardName);
     });
   });
 
diff --git a/superset/assets/spec/javascripts/explore/utils_spec.jsx 
b/superset/assets/spec/javascripts/explore/utils_spec.jsx
index fc4c2b6b3f..8d4f86837f 100644
--- a/superset/assets/spec/javascripts/explore/utils_spec.jsx
+++ b/superset/assets/spec/javascripts/explore/utils_spec.jsx
@@ -1,9 +1,10 @@
 import { it, describe } from 'mocha';
 import { expect } from 'chai';
 import URI from 'urijs';
-import { getExploreUrl } from '../../../javascripts/explore/exploreUtils';
+import { getExploreUrlAndPayload, getExploreLongUrl } from 
'../../../javascripts/explore/exploreUtils';
 
 describe('utils', () => {
+  const location = window.location;
   const formData = {
     datasource: '1__table',
   };
@@ -12,48 +13,129 @@ describe('utils', () => {
     expect(uri1.toString()).to.equal(uri2.toString());
   }
 
-  it('getExploreUrl generates proper base url', () => {
-    // This assertion is to show clearly the value of location.href
-    // in the context of unit tests.
-    expect(location.href).to.equal('about:blank');
+  describe('getExploreUrlAndPayload', () => {
+    it('generates proper base url', () => {
+      // This assertion is to show clearly the value of location.href
+      // in the context of unit tests.
+      expect(location.href).to.equal('about:blank');
 
-    compareURI(
-        URI(getExploreUrl(formData, 'base', false, 'http://superset.com')),
-        URI('/superset/explore/table/1/').search({ form_data: sFormData }),
-    );
-  });
-  it('getExploreUrl generates proper json url', () => {
-    compareURI(
-        URI(getExploreUrl(formData, 'json', false, 'superset.com')),
-        URI('/superset/explore_json/table/1/').search({ form_data: sFormData 
}),
-    );
-  });
-  it('getExploreUrl generates proper json forced url', () => {
-    compareURI(
-        URI(getExploreUrl(formData, 'json', true, 'superset.com')),
+      const { url, payload } = getExploreUrlAndPayload({
+        formData,
+        endpointType: 'base',
+        force: false,
+        curUrl: 'http://superset.com',
+      });
+      compareURI(
+        URI(url),
+        URI('/superset/explore/table/1/'),
+      );
+      expect(payload).to.deep.equals(formData);
+    });
+    it('generates proper json url', () => {
+      const { url, payload } = getExploreUrlAndPayload({
+        formData,
+        endpointType: 'json',
+        force: false,
+        curUrl: 'http://superset.com',
+      });
+      compareURI(
+        URI(url),
+        URI('/superset/explore_json/table/1/'),
+      );
+      expect(payload).to.deep.equals(formData);
+    });
+    it('generates proper json forced url', () => {
+      const { url, payload } = getExploreUrlAndPayload({
+        formData,
+        endpointType: 'json',
+        force: true,
+        curUrl: 'superset.com',
+      });
+      compareURI(
+        URI(url),
         URI('/superset/explore_json/table/1/')
-          .search({ form_data: sFormData, force: 'true' }),
-    );
-  });
-  it('getExploreUrl generates proper csv URL', () => {
-    compareURI(
-        URI(getExploreUrl(formData, 'csv', false, 'superset.com')),
+          .search({ force: 'true' }),
+      );
+      expect(payload).to.deep.equals(formData);
+    });
+    it('generates proper csv URL', () => {
+      const { url, payload } = getExploreUrlAndPayload({
+        formData,
+        endpointType: 'csv',
+        force: false,
+        curUrl: 'superset.com',
+      });
+      compareURI(
+        URI(url),
         URI('/superset/explore_json/table/1/')
-          .search({ form_data: sFormData, csv: 'true' }),
-    );
-  });
-  it('getExploreUrl generates proper standalone URL', () => {
-    compareURI(
-        URI(getExploreUrl(formData, 'standalone', false, 'superset.com')),
+          .search({ csv: 'true' }),
+      );
+      expect(payload).to.deep.equals(formData);
+    });
+    it('generates proper standalone URL', () => {
+      const { url, payload } = getExploreUrlAndPayload({
+        formData,
+        endpointType: 'standalone',
+        force: false,
+        curUrl: 'superset.com',
+      });
+      compareURI(
+        URI(url),
         URI('/superset/explore/table/1/')
-          .search({ form_data: sFormData, standalone: 'true' }),
-    );
-  });
-  it('getExploreUrl preserves main URLs params', () => {
-    compareURI(
-        URI(getExploreUrl(formData, 'json', false, 'superset.com?foo=bar')),
+          .search({ standalone: 'true' }),
+      );
+      expect(payload).to.deep.equals(formData);
+    });
+    it('preserves main URLs params', () => {
+      const { url, payload } = getExploreUrlAndPayload({
+        formData,
+        endpointType: 'json',
+        force: false,
+        curUrl: 'superset.com?foo=bar',
+      });
+      compareURI(
+        URI(url),
         URI('/superset/explore_json/table/1/')
-          .search({ foo: 'bar', form_data: sFormData }),
-    );
+          .search({ foo: 'bar' }),
+      );
+      expect(payload).to.deep.equals(formData);
+    });
+    it('generate proper save slice url', () => {
+      const { url, payload } = getExploreUrlAndPayload({
+        formData,
+        endpointType: 'json',
+        force: false,
+        curUrl: 'superset.com?foo=bar',
+      });
+      compareURI(
+        URI(url),
+        URI('/superset/explore_json/table/1/')
+          .search({ foo: 'bar' }),
+      );
+      expect(payload).to.deep.equals(formData);
+    });
+    it('generate proper saveas slice url', () => {
+      const { url, payload } = getExploreUrlAndPayload({
+        formData,
+        endpointType: 'json',
+        force: false,
+        curUrl: 'superset.com?foo=bar',
+      });
+      compareURI(
+        URI(url),
+        URI('/superset/explore_json/table/1/')
+          .search({ foo: 'bar' }),
+      );
+      expect(payload).to.deep.equals(formData);
+    });
+  });
+
+  describe('getExploreLongUrl', () => {
+    it('generates proper base url with form_data', () => {
+      compareURI(
+        URI(getExploreLongUrl(formData, 'base')),
+        URI('/superset/explore/table/1/').search({ form_data: sFormData }),
+      );
+    });
   });
 });
diff --git a/superset/assets/spec/javascripts/sqllab/VisualizeModal_spec.jsx 
b/superset/assets/spec/javascripts/sqllab/VisualizeModal_spec.jsx
index dffa8250ba..6c9fc5b1ed 100644
--- a/superset/assets/spec/javascripts/sqllab/VisualizeModal_spec.jsx
+++ b/superset/assets/spec/javascripts/sqllab/VisualizeModal_spec.jsx
@@ -308,17 +308,17 @@ describe('VisualizeModal', () => {
     beforeEach(() => {
       ajaxSpy = sinon.spy($, 'ajax');
       sinon.stub(JSON, 'parse').callsFake(() => ({ table_id: 107 }));
-      sinon.stub(exploreUtils, 'getExploreUrl').callsFake(() => ('mockURL'));
+      sinon.stub(exploreUtils, 'getExploreUrlAndPayload').callsFake(() => ({ 
url: 'mockURL', payload: { datasource: '107__table' } }));
+      sinon.spy(exploreUtils, 'exportChart');
       sinon.stub(wrapper.instance(), 'buildVizOptions').callsFake(() => 
(mockOptions));
-      sinon.spy(window, 'open');
       datasourceSpy = sinon.stub(actions, 'createDatasource');
     });
     afterEach(() => {
       ajaxSpy.restore();
       JSON.parse.restore();
-      exploreUtils.getExploreUrl.restore();
+      exploreUtils.getExploreUrlAndPayload.restore();
+      exploreUtils.exportChart.restore();
       wrapper.instance().buildVizOptions.restore();
-      window.open.restore();
       datasourceSpy.restore();
     });
 
@@ -340,9 +340,8 @@ describe('VisualizeModal', () => {
       wrapper.setProps({ actions: { createDatasource: datasourceSpy } });
 
       wrapper.instance().visualize();
-      expect(exploreUtils.getExploreUrl.callCount).to.equal(1);
-      
expect(exploreUtils.getExploreUrl.getCall(0).args[0].datasource).to.equal('107__table');
-      expect(window.open.callCount).to.equal(1);
+      expect(exploreUtils.exportChart.callCount).to.equal(1);
+      
expect(exploreUtils.exportChart.getCall(0).args[0].datasource).to.equal('107__table');
     });
     it('should notify error', () => {
       datasourceSpy.callsFake(() => {
@@ -354,7 +353,7 @@ describe('VisualizeModal', () => {
       sinon.spy(notify, 'error');
 
       wrapper.instance().visualize();
-      expect(window.open.callCount).to.equal(0);
+      expect(exploreUtils.exportChart.callCount).to.equal(0);
       expect(notify.error.callCount).to.equal(1);
     });
   });
diff --git a/superset/assets/visualizations/deckgl/DeckGLContainer.jsx 
b/superset/assets/visualizations/deckgl/DeckGLContainer.jsx
index 3166917744..1b7ca317ca 100644
--- a/superset/assets/visualizations/deckgl/DeckGLContainer.jsx
+++ b/superset/assets/visualizations/deckgl/DeckGLContainer.jsx
@@ -33,6 +33,7 @@ export default class DeckGLContainer extends React.Component {
   componentWillReceiveProps(nextProps) {
     this.setState(() => ({
       viewport: { ...nextProps.viewport },
+      previousViewport: this.state.viewport,
     }));
   }
   componentWillUnmount() {
diff --git a/superset/assets/visualizations/deckgl/layers/arc.jsx 
b/superset/assets/visualizations/deckgl/layers/arc.jsx
index ebeff3cb89..43583e0170 100644
--- a/superset/assets/visualizations/deckgl/layers/arc.jsx
+++ b/superset/assets/visualizations/deckgl/layers/arc.jsx
@@ -8,6 +8,15 @@ import DeckGLContainer from './../DeckGLContainer';
 import * as common from './common';
 import sandboxedEval from '../../../javascripts/modules/sandbox';
 
+function getPoints(data) {
+  const points = [];
+  data.forEach((d) => {
+    points.push(d.sourcePosition);
+    points.push(d.targetPosition);
+  });
+  return points;
+}
+
 function getLayer(formData, payload, slice) {
   const fd = formData;
   const fc = fd.color_picker;
@@ -32,11 +41,15 @@ function getLayer(formData, payload, slice) {
 
 function deckArc(slice, payload, setControlValue) {
   const layer = getLayer(slice.formData, payload, slice);
-  const viewport = {
+  let viewport = {
     ...slice.formData.viewport,
     width: slice.width(),
     height: slice.height(),
   };
+
+  if (slice.formData.autozoom) {
+    viewport = common.fitViewport(viewport, getPoints(payload.data.arcs));
+  }
   ReactDOM.render(
     <DeckGLContainer
       mapboxApiAccessToken={payload.data.mapboxApiKey}
diff --git a/superset/assets/visualizations/deckgl/layers/common.js 
b/superset/assets/visualizations/deckgl/layers/common.js
index 7f11213b6c..4692401cf8 100644
--- a/superset/assets/visualizations/deckgl/layers/common.js
+++ b/superset/assets/visualizations/deckgl/layers/common.js
@@ -1,6 +1,30 @@
 import dompurify from 'dompurify';
+import { fitBounds } from 'viewport-mercator-project';
+
 import sandboxedEval from '../../../javascripts/modules/sandbox';
 
+export function getBounds(points) {
+  const latExt = d3.extent(points, d => d[1]);
+  const lngExt = d3.extent(points, d => d[0]);
+  return [
+    [lngExt[0], latExt[0]],
+    [lngExt[1], latExt[1]],
+  ];
+}
+
+export function fitViewport(viewport, points, padding = 10) {
+  const bounds = getBounds(points);
+  return {
+    ...viewport,
+    ...fitBounds({
+      height: viewport.height,
+      width: viewport.width,
+      padding,
+      bounds,
+    }),
+  };
+}
+
 export function commonLayerProps(formData, slice) {
   const fd = formData;
   let onHover;
diff --git a/superset/assets/visualizations/deckgl/layers/geojson.jsx 
b/superset/assets/visualizations/deckgl/layers/geojson.jsx
index 7f0936304e..fe7805a279 100644
--- a/superset/assets/visualizations/deckgl/layers/geojson.jsx
+++ b/superset/assets/visualizations/deckgl/layers/geojson.jsx
@@ -1,10 +1,9 @@
 import React from 'react';
 import ReactDOM from 'react-dom';
-
 import { GeoJsonLayer } from 'deck.gl';
+// TODO import geojsonExtent from 'geojson-extent';
 
 import DeckGLContainer from './../DeckGLContainer';
-
 import * as common from './common';
 import { hexToRGB } from '../../../javascripts/modules/colors';
 import sandboxedEval from '../../../javascripts/modules/sandbox';
@@ -100,6 +99,11 @@ function deckGeoJson(slice, payload, setControlValue) {
     width: slice.width(),
     height: slice.height(),
   };
+  if (slice.formData.autozoom) {
+    // TODO get this to work
+    // viewport = common.fitViewport(viewport, 
geojsonExtent(payload.data.features));
+  }
+
   ReactDOM.render(
     <DeckGLContainer
       mapboxApiAccessToken={payload.data.mapboxApiKey}
diff --git a/superset/assets/visualizations/deckgl/layers/grid.jsx 
b/superset/assets/visualizations/deckgl/layers/grid.jsx
index 1e7ff1d70b..6f92f0688c 100644
--- a/superset/assets/visualizations/deckgl/layers/grid.jsx
+++ b/superset/assets/visualizations/deckgl/layers/grid.jsx
@@ -37,13 +37,22 @@ function getLayer(formData, payload, slice) {
   });
 }
 
+function getPoints(data) {
+  return data.map(d => d.position);
+}
+
 function deckGrid(slice, payload, setControlValue) {
   const layer = getLayer(slice.formData, payload, slice);
-  const viewport = {
+  let viewport = {
     ...slice.formData.viewport,
     width: slice.width(),
     height: slice.height(),
   };
+
+  if (slice.formData.autozoom) {
+    viewport = common.fitViewport(viewport, getPoints(payload.data.features));
+  }
+
   ReactDOM.render(
     <DeckGLContainer
       mapboxApiAccessToken={payload.data.mapboxApiKey}
diff --git a/superset/assets/visualizations/deckgl/layers/hex.jsx 
b/superset/assets/visualizations/deckgl/layers/hex.jsx
index 7dc4d8b025..f0fb925da5 100644
--- a/superset/assets/visualizations/deckgl/layers/hex.jsx
+++ b/superset/assets/visualizations/deckgl/layers/hex.jsx
@@ -37,13 +37,22 @@ function getLayer(formData, payload, slice) {
   });
 }
 
+function getPoints(data) {
+  return data.map(d => d.position);
+}
+
 function deckHex(slice, payload, setControlValue) {
   const layer = getLayer(slice.formData, payload, slice);
-  const viewport = {
+  let viewport = {
     ...slice.formData.viewport,
     width: slice.width(),
     height: slice.height(),
   };
+
+  if (slice.formData.autozoom) {
+    viewport = common.fitViewport(viewport, getPoints(payload.data.features));
+  }
+
   ReactDOM.render(
     <DeckGLContainer
       mapboxApiAccessToken={payload.data.mapboxApiKey}
diff --git a/superset/assets/visualizations/deckgl/layers/path.jsx 
b/superset/assets/visualizations/deckgl/layers/path.jsx
index a20c2bbdb0..4b45a0ba13 100644
--- a/superset/assets/visualizations/deckgl/layers/path.jsx
+++ b/superset/assets/visualizations/deckgl/layers/path.jsx
@@ -33,13 +33,26 @@ function getLayer(formData, payload, slice) {
   });
 }
 
+function getPoints(data) {
+  let points = [];
+  data.forEach((d) => {
+    points = points.concat(d.path);
+  });
+  return points;
+}
+
 function deckPath(slice, payload, setControlValue) {
   const layer = getLayer(slice.formData, payload, slice);
-  const viewport = {
+  let viewport = {
     ...slice.formData.viewport,
     width: slice.width(),
     height: slice.height(),
   };
+
+  if (slice.formData.autozoom) {
+    viewport = common.fitViewport(viewport, getPoints(payload.data.features));
+  }
+
   ReactDOM.render(
     <DeckGLContainer
       mapboxApiAccessToken={payload.data.mapboxApiKey}
diff --git a/superset/assets/visualizations/deckgl/layers/scatter.jsx 
b/superset/assets/visualizations/deckgl/layers/scatter.jsx
index ed1dd792af..646a9afef7 100644
--- a/superset/assets/visualizations/deckgl/layers/scatter.jsx
+++ b/superset/assets/visualizations/deckgl/layers/scatter.jsx
@@ -1,15 +1,17 @@
 import React from 'react';
 import ReactDOM from 'react-dom';
-
 import { ScatterplotLayer } from 'deck.gl';
 
 import DeckGLContainer from './../DeckGLContainer';
-
 import * as common from './common';
 import { getColorFromScheme, hexToRGB } from 
'../../../javascripts/modules/colors';
 import { unitToRadius } from '../../../javascripts/modules/geo';
 import sandboxedEval from '../../../javascripts/modules/sandbox';
 
+function getPoints(data) {
+  return data.map(d => d.position);
+}
+
 function getLayer(formData, payload, slice) {
   const fd = formData;
   const c = fd.color_picker || { r: 0, g: 0, b: 0, a: 1 };
@@ -50,17 +52,25 @@ function getLayer(formData, payload, slice) {
 
 function deckScatter(slice, payload, setControlValue) {
   const layer = getLayer(slice.formData, payload, slice);
-  const viewport = {
-    ...slice.formData.viewport,
-    width: slice.width(),
-    height: slice.height(),
+  const fd = slice.formData;
+  const width = slice.width();
+  const height = slice.height();
+  let viewport = {
+    ...fd.viewport,
+    width,
+    height,
   };
+
+  if (fd.autozoom) {
+    viewport = common.fitViewport(viewport, getPoints(payload.data.features));
+  }
+
   ReactDOM.render(
     <DeckGLContainer
       mapboxApiAccessToken={payload.data.mapboxApiKey}
       viewport={viewport}
       layers={[layer]}
-      mapStyle={slice.formData.mapbox_style}
+      mapStyle={fd.mapbox_style}
       setControlValue={setControlValue}
     />,
     document.getElementById(slice.containerId),
diff --git a/superset/assets/visualizations/deckgl/layers/screengrid.jsx 
b/superset/assets/visualizations/deckgl/layers/screengrid.jsx
index 7494c67d3d..7d6742e6e8 100644
--- a/superset/assets/visualizations/deckgl/layers/screengrid.jsx
+++ b/superset/assets/visualizations/deckgl/layers/screengrid.jsx
@@ -37,13 +37,20 @@ function getLayer(formData, payload, slice) {
   });
 }
 
+function getPoints(data) {
+  return data.map(d => d.position);
+}
+
 function deckScreenGrid(slice, payload, setControlValue) {
   const layer = getLayer(slice.formData, payload, slice);
-  const viewport = {
+  let viewport = {
     ...slice.formData.viewport,
     width: slice.width(),
     height: slice.height(),
   };
+  if (slice.formData.autozoom) {
+    viewport = common.fitViewport(viewport, getPoints(payload.data.features));
+  }
   ReactDOM.render(
     <DeckGLContainer
       mapboxApiAccessToken={payload.data.mapboxApiKey}
diff --git a/superset/data/__init__.py b/superset/data/__init__.py
index e1d0b67558..be89c1a7ec 100644
--- a/superset/data/__init__.py
+++ b/superset/data/__init__.py
@@ -1266,10 +1266,10 @@ def load_deck_dash():
         "point_radius_fixed": {"type": "metric", "value": "count"},
         "point_unit": "square_m",
         "row_limit": 5000,
-        "since": "2014-01-01",
+        "since": None,
         "size": "count",
         "time_grain_sqla": "Time Column",
-        "until": "now",
+        "until": None,
         "viewport": {
             "bearing": -4.952916738791771,
             "latitude": 37.78926922909199,
@@ -1305,9 +1305,9 @@ def load_deck_dash():
         "granularity_sqla": "date",
         "size": "count",
         "viz_type": "deck_screengrid",
-        "since": "2014-01-01",
+        "since": None,
         "point_radius": "Auto",
-        "until": "now",
+        "until": None,
         "color_picker": {
             "a": 1,
             "r": 14,
@@ -1352,10 +1352,10 @@ def load_deck_dash():
         "granularity_sqla": "date",
         "size": "count",
         "viz_type": "deck_hex",
-        "since": "2014-01-01",
+        "since": None,
         "point_radius_unit": "Pixels",
         "point_radius": "Auto",
-        "until": "now",
+        "until": None,
         "color_picker": {
             "a": 1,
             "r": 14,
@@ -1401,10 +1401,10 @@ def load_deck_dash():
         "granularity_sqla": "date",
         "size": "count",
         "viz_type": "deck_grid",
-        "since": "2014-01-01",
+        "since": None,
         "point_radius_unit": "Pixels",
         "point_radius": "Auto",
-        "until": "now",
+        "until": None,
         "color_picker": {
             "a": 1,
             "r": 14,
@@ -1446,8 +1446,8 @@ def load_deck_dash():
             "slice_id": 41,
             "granularity_sqla": None,
             "time_grain_sqla": None,
-            "since": "7 days ago",
-            "until": "now",
+            "since": None,
+            "until": None,
             "line_column": "contour",
             "line_type": "json",
             "mapbox_style": "mapbox://styles/mapbox/light-v9",
@@ -1515,8 +1515,8 @@ def load_deck_dash():
             "slice_id": 42,
             "granularity_sqla": "date",
             "time_grain_sqla": "Time Column",
-            "since": "2014-01-01",
-            "until": "now",
+            "since": None,
+            "until": None,
             "start_spatial": {
                 "type": "latlong",
                 "latCol": "LATITUDE",
@@ -1573,8 +1573,8 @@ def load_deck_dash():
         "slice_id": 43,
         "viz_type": "deck_path",
         "time_grain_sqla": "Time Column",
-        "since": "7 days ago",
-        "until": "now",
+        "since": None,
+        "until": None,
         "line_column": "path_json",
         "line_type": "json",
         "row_limit": 5000,
diff --git a/superset/models/core.py b/superset/models/core.py
index cfd6d75203..df45ccf533 100644
--- a/superset/models/core.py
+++ b/superset/models/core.py
@@ -209,10 +209,11 @@ def form_data(self):
     @property
     def slice_url(self):
         """Defines the url to access the slice"""
+        form_data = {'slice_id': self.id}
         return (
             '/superset/explore/{obj.datasource_type}/'
             '{obj.datasource_id}/?form_data={params}'.format(
-                obj=self, params=parse.quote(json.dumps(self.form_data))))
+                obj=self, params=parse.quote(json.dumps(form_data))))
 
     @property
     def slice_id_url(self):
@@ -860,9 +861,10 @@ def wrapper(*args, **kwargs):
             user_id = None
             if g.user:
                 user_id = g.user.get_id()
-            d = request.args.to_dict()
-            post_data = request.form.to_dict() or {}
-            d.update(post_data)
+            d = request.form.to_dict() or {}
+            # request parameters can overwrite post body
+            request_params = request.args.to_dict()
+            d.update(request_params)
             d.update(kwargs)
             slice_id = d.get('slice_id')
 
diff --git a/superset/utils.py b/superset/utils.py
index 42616e72a2..f4fcd93d07 100644
--- a/superset/utils.py
+++ b/superset/utils.py
@@ -716,7 +716,7 @@ def wraps(self, *args, **kwargs):
         return redirect(
             url_for(
                 self.appbuilder.sm.auth_view.__class__.__name__ + '.login',
-                next=request.path))
+                next=request.full_path))
 
     f._permission_name = permission_str
     return functools.update_wrapper(wraps, f)
diff --git a/superset/views/core.py b/superset/views/core.py
index 87adb6e5eb..a5d689457a 100755
--- a/superset/views/core.py
+++ b/superset/views/core.py
@@ -718,11 +718,12 @@ def index(self, url_id):
     @expose('/shortner/', methods=['POST', 'GET'])
     def shortner(self):
         url = request.form.get('data')
+        directory = url.split('?')[0][2:]
         obj = models.Url(url=url)
         db.session.add(obj)
         db.session.commit()
-        return('http://{request.headers[Host]}/r/{obj.id}'.format(
-            request=request, obj=obj))
+        return('http://{request.headers[Host]}/{directory}?r={obj.id}'.format(
+            request=request, directory=directory, obj=obj))
 
     @expose('/msg/')
     def msg(self):
@@ -928,16 +929,15 @@ def clean_fulfilled_requests(session):
         return redirect('/accessrequestsmodelview/list/')
 
     def get_form_data(self):
-        # get form data from url
-        if request.args.get('form_data'):
-            form_data = request.args.get('form_data')
-        elif request.form.get('form_data'):
-            # Supporting POST as well as get
-            form_data = request.form.get('form_data')
-        else:
-            form_data = '{}'
-
-        d = json.loads(form_data)
+        d = {}
+        post_data = request.form.get('form_data')
+        request_args_data = request.args.get('form_data')
+        # Supporting POST
+        if post_data:
+            d.update(json.loads(post_data))
+        # request params can overwrite post body
+        if request_args_data:
+            d.update(json.loads(request_args_data))
 
         if request.args.get('viz_type'):
             # Converting old URLs
@@ -1096,7 +1096,7 @@ def annotation_json(self, layer_id):
 
     @log_this
     @has_access_api
-    @expose('/explore_json/<datasource_type>/<datasource_id>/')
+    @expose('/explore_json/<datasource_type>/<datasource_id>/', 
methods=['GET', 'POST'])
     def explore_json(self, datasource_type, datasource_id):
         try:
             csv = request.args.get('csv') == 'true'
@@ -1147,18 +1147,31 @@ def explorev2(self, datasource_type, datasource_id):
 
     @log_this
     @has_access
-    @expose('/explore/<datasource_type>/<datasource_id>/')
+    @expose('/explore/<datasource_type>/<datasource_id>/', methods=['GET', 
'POST'])
     def explore(self, datasource_type, datasource_id):
-        form_data = self.get_form_data()
-
         datasource_id = int(datasource_id)
-        viz_type = form_data.get('viz_type')
-        slice_id = form_data.get('slice_id')
         user_id = g.user.get_id() if g.user else None
+        form_data = self.get_form_data()
 
+        saved_url = None
+        url_id = request.args.get('r')
+        if url_id:
+            saved_url = 
db.session.query(models.Url).filter_by(id=url_id).first()
+            if saved_url:
+                url_str = parse.unquote_plus(
+                    saved_url.url.split('?')[1][10:], encoding='utf-8', 
errors=None)
+                url_form_data = json.loads(url_str)
+                # allow form_date in request override saved url
+                url_form_data.update(form_data)
+                form_data = url_form_data
+        slice_id = form_data.get('slice_id')
         slc = None
         if slice_id:
             slc = db.session.query(models.Slice).filter_by(id=slice_id).first()
+            slice_form_data = slc.form_data.copy()
+            # allow form_data in request override slice from_data
+            slice_form_data.update(form_data)
+            form_data = slice_form_data
 
         error_redirect = '/slicemodelview/list/'
         datasource = ConnectorRegistry.get_datasource(
@@ -1177,6 +1190,7 @@ def explore(self, datasource_type, datasource_id):
                 'datasource_id={datasource_id}&'
                 ''.format(**locals()))
 
+        viz_type = form_data.get('viz_type')
         if not viz_type and datasource.default_endpoint:
             return redirect(datasource.default_endpoint)
 
@@ -1187,6 +1201,9 @@ def explore(self, datasource_type, datasource_id):
 
         form_data['datasource'] = str(datasource_id) + '__' + datasource_type
 
+        # On explore, merge extra filters into the form data
+        merge_extra_filters(form_data)
+
         # handle save or overwrite
         action = request.args.get('action')
 
@@ -1210,11 +1227,6 @@ def explore(self, datasource_type, datasource_id):
                 datasource_type,
                 datasource.name)
 
-        form_data['datasource'] = str(datasource_id) + '__' + datasource_type
-
-        # On explore, merge extra filters into the form data
-        merge_extra_filters(form_data)
-
         standalone = request.args.get('standalone') == 'true'
         bootstrap_data = {
             'can_add': slice_add_perm,
diff --git a/superset/viz.py b/superset/viz.py
index 5c36e40891..c09b3cf51a 100644
--- a/superset/viz.py
+++ b/superset/viz.py
@@ -291,7 +291,6 @@ def get_payload(self, query_obj=None):
         if df is not None and len(df.index) == 0:
             raise Exception('No data')
         payload['data'] = self.get_data(df)
-
         del payload['df']
         return payload
 
@@ -1932,7 +1931,6 @@ def process_spatial_data_obj(self, key, df):
         spatial = self.form_data.get(key)
         if spatial is None:
             raise ValueError(_('Bad spatial key'))
-
         if spatial.get('type') == 'latlong':
             df[key] = list(zip(df[spatial.get('lonCol')], 
df[spatial.get('latCol')]))
         elif spatial.get('type') == 'delimited':
diff --git a/tests/core_tests.py b/tests/core_tests.py
index 8b4dd27ee3..edd7ff580c 100644
--- a/tests/core_tests.py
+++ b/tests/core_tests.py
@@ -100,10 +100,10 @@ def test_slice_json_endpoint(self):
         slc = self.get_slice('Girls', db.session)
 
         json_endpoint = (
-            '/superset/explore_json/{}/{}?form_data={}'
-            .format(slc.datasource_type, slc.datasource_id, 
json.dumps(slc.viz.form_data))
+            '/superset/explore_json/{}/{}/'
+            .format(slc.datasource_type, slc.datasource_id)
         )
-        resp = self.get_resp(json_endpoint)
+        resp = self.get_resp(json_endpoint, {'form_data': 
json.dumps(slc.viz.form_data)})
         assert '"Jennifer"' in resp
 
     def test_slice_csv_endpoint(self):
@@ -111,10 +111,10 @@ def test_slice_csv_endpoint(self):
         slc = self.get_slice('Girls', db.session)
 
         csv_endpoint = (
-            '/superset/explore_json/{}/{}?csv=true&form_data={}'
-            .format(slc.datasource_type, slc.datasource_id, 
json.dumps(slc.viz.form_data))
+            '/superset/explore_json/{}/{}/?csv=true'
+            .format(slc.datasource_type, slc.datasource_id)
         )
-        resp = self.get_resp(csv_endpoint)
+        resp = self.get_resp(csv_endpoint, {'form_data': 
json.dumps(slc.viz.form_data)})
         assert 'Jennifer,' in resp
 
     def test_admin_only_permissions(self):
@@ -155,7 +155,7 @@ def test_save_slice(self):
 
         url = (
             '/superset/explore/table/{}/?slice_name={}&'
-            'action={}&datasource_name=energy_usage&form_data={}')
+            'action={}&datasource_name=energy_usage')
 
         form_data = {
             'viz_type': 'sankey',
@@ -170,8 +170,8 @@ def test_save_slice(self):
                 tbl_id,
                 copy_name,
                 'saveas',
-                json.dumps(form_data),
             ),
+            {'form_data': json.dumps(form_data)},
         )
         slices = db.session.query(models.Slice) \
             .filter_by(slice_name=copy_name).all()
@@ -191,8 +191,8 @@ def test_save_slice(self):
                 tbl_id,
                 new_slice_name,
                 'overwrite',
-                json.dumps(form_data),
             ),
+            {'form_data': json.dumps(form_data)},
         )
         slc = db.session.query(models.Slice).filter_by(id=new_slice_id).first()
         assert slc.slice_name == new_slice_name
@@ -375,8 +375,8 @@ def test_shortner(self):
             'energy_usage&datasource_id=1&datasource_type=table&'
             'previous_viz_type=sankey'
         )
-        resp = self.client.post('/r/shortner/', data=data)
-        assert '/r/' in resp.data.decode('utf-8')
+        resp = self.client.post('/r/shortner/', data=dict(data=data))
+        assert '?r=' in resp.data.decode('utf-8')
 
     def test_kv(self):
         self.logout()
@@ -780,7 +780,7 @@ def 
test_slice_id_is_always_logged_correctly_on_web_request(self):
         # superset/explore case
         slc = 
db.session.query(models.Slice).filter_by(slice_name='Girls').one()
         qry = db.session.query(models.Log).filter_by(slice_id=slc.id)
-        self.get_resp(slc.slice_url)
+        self.get_resp(slc.slice_url, {'form_data': 
json.dumps(slc.viz.form_data)})
         self.assertEqual(1, qry.count())
 
     def test_slice_id_is_always_logged_correctly_on_ajax_request(self):
@@ -789,7 +789,7 @@ def 
test_slice_id_is_always_logged_correctly_on_ajax_request(self):
         slc = 
db.session.query(models.Slice).filter_by(slice_name='Girls').one()
         qry = db.session.query(models.Log).filter_by(slice_id=slc.id)
         slc_url = slc.slice_url.replace('explore', 'explore_json')
-        self.get_json_resp(slc_url)
+        self.get_json_resp(slc_url, {'form_data': 
json.dumps(slc.viz.form_data)})
         self.assertEqual(1, qry.count())
 
     def test_slice_query_endpoint(self):
diff --git a/tests/druid_tests.py b/tests/druid_tests.py
index c280da790a..ee8cfba5f6 100644
--- a/tests/druid_tests.py
+++ b/tests/druid_tests.py
@@ -136,11 +136,8 @@ def test_client(self, PyDruid):
             'force': 'true',
         }
         # One groupby
-        url = (
-            '/superset/explore_json/druid/{}/?form_data={}'.format(
-                datasource_id, json.dumps(form_data))
-        )
-        resp = self.get_json_resp(url)
+        url = ('/superset/explore_json/druid/{}/'.format(datasource_id))
+        resp = self.get_json_resp(url, {'form_data': json.dumps(form_data)})
         self.assertEqual('Canada', resp['data']['records'][0]['dim1'])
 
         form_data = {
@@ -156,11 +153,8 @@ def test_client(self, PyDruid):
             'force': 'true',
         }
         # two groupby
-        url = (
-            '/superset/explore_json/druid/{}/?form_data={}'.format(
-                datasource_id, json.dumps(form_data))
-        )
-        resp = self.get_json_resp(url)
+        url = ('/superset/explore_json/druid/{}/'.format(datasource_id))
+        resp = self.get_json_resp(url, {'form_data': json.dumps(form_data)})
         self.assertEqual('Canada', resp['data']['records'][0]['dim1'])
 
     def test_druid_sync_from_config(self):


 

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


With regards,
Apache Git Services

Reply via email to