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