diff --git a/superset/assets/javascripts/explorev2/components/ChartContainer.jsx b/superset/assets/javascripts/explorev2/components/ChartContainer.jsx index 6599ff5d278f5..c00b82217239f 100644 --- a/superset/assets/javascripts/explorev2/components/ChartContainer.jsx +++ b/superset/assets/javascripts/explorev2/components/ChartContainer.jsx @@ -31,6 +31,8 @@ const propTypes = { slice: PropTypes.object.isRequired, table_name: PropTypes.string, viz_type: PropTypes.string.isRequired, + formData: PropTypes.object, + latestQueryFormData: PropTypes.object, }; class ChartContainer extends React.PureComponent { @@ -226,13 +228,12 @@ class ChartContainer extends React.PureComponent { status={CHART_STATUS_MAP[this.props.chartStatus]} style={{ fontSize: '10px', marginRight: '5px' }} /> - {this.state.mockSlice && - - } + } @@ -256,8 +257,8 @@ function mapStateToProps(state) { chartUpdateStartTime: state.chartUpdateStartTime, column_formats: state.datasource ? state.datasource.column_formats : null, containerId: state.slice ? `slice-container-${state.slice.slice_id}` : 'slice-container', - datasource_type: state.datasource_type, formData, + latestQueryFormData: state.latestQueryFormData, isStarred: state.isStarred, queryResponse: state.queryResponse, slice: state.slice, diff --git a/superset/assets/javascripts/explorev2/components/DisplayQueryButton.jsx b/superset/assets/javascripts/explorev2/components/DisplayQueryButton.jsx index 990c0930cd463..05fe4946f32e0 100644 --- a/superset/assets/javascripts/explorev2/components/DisplayQueryButton.jsx +++ b/superset/assets/javascripts/explorev2/components/DisplayQueryButton.jsx @@ -1,25 +1,49 @@ import React, { PropTypes } from 'react'; import ModalTrigger from './../../components/ModalTrigger'; +const $ = window.$ = require('jquery'); const propTypes = { - query: PropTypes.string, + queryEndpoint: PropTypes.string.isRequired, }; -const defaultProps = { - query: '', -}; - -export default function DisplayQueryButton({ query }) { - const modalBody = (
{query}
); - return ( - Query} - modalTitle="Query" - modalBody={modalBody} - /> - ); +export default class DisplayQueryButton extends React.PureComponent { + constructor(props) { + super(props); + this.state = { + modalBody:
,
+    };
+  }
+  beforeOpen() {
+    this.setState({
+      modalBody:
+        (Loading...),
+    });
+    $.ajax({
+      type: 'GET',
+      url: this.props.queryEndpoint,
+      success: (data) => {
+        this.setState({ modalBody: (
{data.query}
) }); + }, + error(data) { + this.setState({ modalBody: (
{data.error}
) }); + }, + }); + } + render() { + return ( + Query} + modalTitle="Query" + beforeOpen={this.beforeOpen.bind(this)} + modalBody={this.state.modalBody} + /> + ); + } } DisplayQueryButton.propTypes = propTypes; -DisplayQueryButton.defaultProps = defaultProps; diff --git a/superset/assets/javascripts/explorev2/components/ExploreActionButtons.jsx b/superset/assets/javascripts/explorev2/components/ExploreActionButtons.jsx index b88410006106b..5823c93bf6f5a 100644 --- a/superset/assets/javascripts/explorev2/components/ExploreActionButtons.jsx +++ b/superset/assets/javascripts/explorev2/components/ExploreActionButtons.jsx @@ -6,40 +6,47 @@ import DisplayQueryButton from './DisplayQueryButton'; const propTypes = { canDownload: PropTypes.oneOfType([PropTypes.string, PropTypes.bool]).isRequired, - slice: PropTypes.object.isRequired, - query: PropTypes.string, + slice: PropTypes.object, + queryEndpoint: PropTypes.string, }; -export default function ExploreActionButtons({ canDownload, slice, query }) { +export default function ExploreActionButtons({ canDownload, slice, queryEndpoint }) { const exportToCSVClasses = cx('btn btn-default btn-sm', { 'disabled disabledButton': !canDownload, }); - return ( -
- + if (slice) { + return ( + + +
+ ); + } + return ( + ); } diff --git a/superset/assets/javascripts/explorev2/exploreUtils.js b/superset/assets/javascripts/explorev2/exploreUtils.js index 71b97260ba373..24b7b60eae466 100644 --- a/superset/assets/javascripts/explorev2/exploreUtils.js +++ b/superset/assets/javascripts/explorev2/exploreUtils.js @@ -12,6 +12,8 @@ export function getExploreUrl(form_data, dummy, endpoint = 'base') { return `/superset/explore_json/${params}&csv=true`; case 'standalone': return `/superset/explore/${params}&standalone=true`; + case 'query': + return `/superset/explore_json/${params}&query=true`; default: return `/superset/explore/${params}`; } diff --git a/superset/assets/javascripts/explorev2/index.jsx b/superset/assets/javascripts/explorev2/index.jsx index db4c8248577de..6ad9e0a09043e 100644 --- a/superset/assets/javascripts/explorev2/index.jsx +++ b/superset/assets/javascripts/explorev2/index.jsx @@ -7,7 +7,7 @@ import { Provider } from 'react-redux'; import thunk from 'redux-thunk'; import { now } from '../modules/dates'; import { initEnhancer } from '../reduxUtils'; -import { getFieldsState } from './stores/store'; +import { getFieldsState, getFormDataFromFields } from './stores/store'; // jquery and bootstrap required to make bootstrap dropdown menu's work @@ -32,6 +32,7 @@ const bootstrappedState = Object.assign( chartUpdateStartTime: now(), dashboards: [], fields, + latestQueryFormData: getFormDataFromFields(fields), filterColumnOpts: [], isDatasourceMetaLoading: false, isStarred: false, diff --git a/superset/assets/javascripts/explorev2/reducers/exploreReducer.js b/superset/assets/javascripts/explorev2/reducers/exploreReducer.js index 0684d6d878b0e..bcb57411bd095 100644 --- a/superset/assets/javascripts/explorev2/reducers/exploreReducer.js +++ b/superset/assets/javascripts/explorev2/reducers/exploreReducer.js @@ -88,6 +88,7 @@ export const exploreReducer = function (state, action) { chartUpdateStartTime: now(), triggerQuery: false, queryRequest: action.queryRequest, + latestQueryFormData: getFormDataFromFields(state.fields), }); }, [actions.CHART_UPDATE_STOPPED]() { diff --git a/superset/models.py b/superset/models.py index 2a9ee8f41707f..94f31e36e54fe 100644 --- a/superset/models.py +++ b/superset/models.py @@ -1237,8 +1237,9 @@ def values_for_column(self, con=engine ) - def query( # sqla - self, groupby, metrics, + def get_query_str( # sqla + self, engine, qry_start_dttm, + groupby, metrics, granularity, from_dttm, to_dttm, filter=None, # noqa @@ -1261,7 +1262,6 @@ def query( # sqla cols = {col.column_name: col for col in self.columns} metrics_dict = {m.metric_name: m for m in self.metrics} - qry_start_dttm = datetime.now() if not granularity and is_timeseries: raise Exception(_( @@ -1416,13 +1416,18 @@ def visit_column(element, compiler, **kw): qry = qry.select_from(tbl) - engine = self.database.get_sqla_engine() sql = "{}".format( qry.compile( engine, compile_kwargs={"literal_binds": True},), ) logging.info(sql) sql = sqlparse.format(sql, reindent=True) + return sql + + def query(self, query_obj): + qry_start_dttm = datetime.now() + engine = self.database.get_sqla_engine() + sql = self.get_query_str(engine, qry_start_dttm, **query_obj) status = QueryStatus.SUCCESS error_message = None df = None @@ -2261,8 +2266,9 @@ def values_for_column(self, return df - def query( # druid - self, groupby, metrics, + def get_query_str( # druid + self, client, qry_start_dttm, + groupby, metrics, granularity, from_dttm, to_dttm, filter=None, # noqa @@ -2274,13 +2280,12 @@ def query( # druid orderby=None, extras=None, # noqa select=None, # noqa - columns=None, ): + columns=None, phase=2): """Runs a query against Druid and returns a dataframe. This query interface is common to SqlAlchemy and Druid """ # TODO refactor into using a TBD Query object - qry_start_dttm = datetime.now() if not is_timeseries: granularity = 'all' inner_from_dttm = inner_from_dttm or from_dttm @@ -2376,7 +2381,6 @@ def recursive_get_fields(_conf): if having_filters: qry['having'] = having_filters - client = self.cluster.get_pydruid_client() orig_filters = filters if len(groupby) == 0: del qry['dimensions'] @@ -2415,6 +2419,8 @@ def recursive_get_fields(_conf): query_str += json.dumps( client.query_builder.last_query.query_dict, indent=2) query_str += "\n" + if phase == 1: + return query_str query_str += ( "//\nPhase 2 (built based on phase one's results)\n") df = client.export_pandas() @@ -2454,15 +2460,23 @@ def recursive_get_fields(_conf): client.groupby(**qry) query_str += json.dumps( client.query_builder.last_query.query_dict, indent=2) + return query_str + + def query(self, query_obj): + qry_start_dttm = datetime.now() + client = self.cluster.get_pydruid_client() + query_str = self.get_query_str(client, qry_start_dttm, **query_obj) df = client.export_pandas() if df is None or df.size == 0: raise Exception(_("No data was returned.")) df.columns = [ DTTM_ALIAS if c == 'timestamp' else c for c in df.columns] + is_timeseries = query_obj['is_timeseries'] \ + if 'is_timeseries' in query_obj else True if ( not is_timeseries and - granularity == "all" and + query_obj['granularity'] == "all" and DTTM_ALIAS in df.columns): del df[DTTM_ALIAS] @@ -2470,11 +2484,11 @@ def recursive_get_fields(_conf): cols = [] if DTTM_ALIAS in df.columns: cols += [DTTM_ALIAS] - cols += [col for col in groupby if col in df.columns] - cols += [col for col in metrics if col in df.columns] + cols += [col for col in query_obj['groupby'] if col in df.columns] + cols += [col for col in query_obj['metrics'] if col in df.columns] df = df[cols] - time_offset = DruidDatasource.time_offset(granularity) + time_offset = DruidDatasource.time_offset(query_obj['granularity']) def increment_timestamp(ts): dt = utils.parse_human_datetime(ts).replace( diff --git a/superset/views.py b/superset/views.py index 48724fd1d67ec..9abac1405fcc3 100755 --- a/superset/views.py +++ b/superset/views.py @@ -1486,6 +1486,24 @@ def explore_json(self, datasource_type, datasource_id): headers=generate_download_headers("csv"), mimetype="application/csv") + if request.args.get("query") == "true": + try: + query_obj = viz_obj.query_obj() + engine = viz_obj.datasource.database.get_sqla_engine() \ + if datasource_type == 'table' \ + else viz_obj.datasource.cluster.get_pydruid_client() + if datasource_type == 'druid': + # only retrive first phase query for druid + query_obj['phase'] = 1 + query = viz_obj.datasource.get_query_str( + engine, datetime.now(), **query_obj) + except Exception as e: + return json_error_response(e) + return Response( + json.dumps({'query': query}), + status=200, + mimetype="application/json") + payload = {} status = 200 try: diff --git a/superset/viz.py b/superset/viz.py index e742e8a694bd4..e9165e0b0a708 100755 --- a/superset/viz.py +++ b/superset/viz.py @@ -106,10 +106,10 @@ def get_df(self, query_obj=None): timestamp_format = dttm_col.python_date_format # The datasource here can be different backend but the interface is common - self.results = self.datasource.query(**query_obj) + self.results = self.datasource.query(query_obj) + self.query = self.results.query self.status = self.results.status self.error_message = self.results.error_message - self.query = self.results.query df = self.results.df # Transform the timestamp we received from database to pandas supported