This is an automated email from the ASF dual-hosted git repository. hugh pushed a commit to branch hugh-v2022.29.0 in repository https://gitbox.apache.org/repos/asf/superset.git
commit cda3ddb135103e4b1d1f26efc33c7a47189470dc Author: hughhhh <[email protected]> AuthorDate: Thu Jul 21 15:04:33 2022 -0400 add fixes --- .../src/SqlLab/components/ResultSet/index.tsx | 42 ++++++++---- superset/models/helpers.py | 80 ++++++++++++++++++++-- superset/utils/core.py | 2 +- superset/views/core.py | 19 ++--- 4 files changed, 116 insertions(+), 27 deletions(-) diff --git a/superset-frontend/src/SqlLab/components/ResultSet/index.tsx b/superset-frontend/src/SqlLab/components/ResultSet/index.tsx index 124d2546a6..7b7780a0b4 100644 --- a/superset-frontend/src/SqlLab/components/ResultSet/index.tsx +++ b/superset-frontend/src/SqlLab/components/ResultSet/index.tsx @@ -29,6 +29,9 @@ import { SaveDatasetModal, } from 'src/SqlLab/components/SaveDatasetModal'; import { UserWithPermissionsAndRoles } from 'src/types/bootstrapTypes'; +import { EXPLORE_CHART_DEFAULT } from 'src/SqlLab/types'; +import { mountExploreUrl } from 'src/explore/exploreUtils'; +import { postFormData } from 'src/explore/exploreUtils/formData'; import ProgressBar from 'src/components/ProgressBar'; import Loading from 'src/components/Loading'; import FilterableTable, { @@ -41,6 +44,7 @@ import ExploreCtasResultsButton from '../ExploreCtasResultsButton'; import ExploreResultsButton from '../ExploreResultsButton'; import HighlightedSql from '../HighlightedSql'; import QueryStateLabel from '../QueryStateLabel'; +import { URL_PARAMS } from 'src/constants'; enum LIMITING_FACTOR { QUERY = 'QUERY', @@ -134,6 +138,8 @@ export default class ResultSet extends React.PureComponent< this.reFetchQueryResults = this.reFetchQueryResults.bind(this); this.toggleExploreResultsButton = this.toggleExploreResultsButton.bind(this); + this.createExploreResultsOnClick = + this.createExploreResultsOnClick.bind(this); } async componentDidMount() { @@ -213,6 +219,26 @@ export default class ResultSet extends React.PureComponent< } } + async createExploreResultsOnClick() { + const { results } = this.props.query; + + if (results.query_id) { + const key = await postFormData(results.query_id, 'query', { + ...EXPLORE_CHART_DEFAULT, + datasource: `${results.query_id}__query`, + ...{ + all_columns: results.columns.map(column => column.name), + }, + }); + const url = mountExploreUrl(null, { + [URL_PARAMS.formDataKey.name]: key, + }); + window.open(url, '_blank', 'noreferrer'); + } else { + this.setState({ showSaveDatasetModal: true }); + } + } + renderControls() { if (this.props.search || this.props.visualize || this.props.csv) { let { data } = this.props.query.results; @@ -250,19 +276,11 @@ export default class ResultSet extends React.PureComponent< this.props.database?.allows_virtual_table_explore && ( <ExploreResultsButton database={this.props.database} - onClick={() => { - // There is currently redux / state issue where sometimes a query will have serverId - // and other times it will not. We need this attribute consistently for this to work - // const qid = this.props?.query?.results?.query_id; - // if (qid) { - // // This will open explore using the query as datasource - // window.location.href = `/explore/?dataset_type=query&dataset_id=${qid}`; - // } else { - // this.setState({ showSaveDatasetModal: true }); - // } - this.setState({ showSaveDatasetModal: true }); - }} + onClick={() => this.setState({ showSaveDatasetModal: true })} /> + // In order to use the new workflow for a query powered chart, replace the + // above function with: + // onClick={this.createExploreResultsOnClick} )} {this.props.csv && ( <Button buttonSize="small" href={`/superset/csv/${query.id}`}> diff --git a/superset/models/helpers.py b/superset/models/helpers.py index 77707201b5..9895e6e1cd 100644 --- a/superset/models/helpers.py +++ b/superset/models/helpers.py @@ -66,6 +66,7 @@ from superset import app, is_feature_enabled, security_manager from superset.advanced_data_type.types import AdvancedDataTypeResponse from superset.common.db_query_status import QueryStatus from superset.constants import EMPTY_STRING, NULL_STRING +from superset.db_engine_specs.base import TimestampExpression from superset.errors import ErrorLevel, SupersetError, SupersetErrorType from superset.exceptions import ( AdvancedDataTypeResponseError, @@ -1189,6 +1190,62 @@ class ExploreMixin: # pylint: disable=too-many-public-methods return or_(*groups) + def values_for_column(self, column_name: str, limit: int = 10000) -> List[Any]: + """Runs query against sqla to retrieve some + sample values for the given column. + """ + cols = {} + for col in self.columns: + if isinstance(col, dict): + cols[col.get("column_name")] = col + else: + cols[col.column_name] = col + + target_col = cols[column_name] + tp = None # todo(hughhhh): add back self.get_template_processor() + tbl, cte = self.get_from_clause(tp) + + if isinstance(target_col, dict): + sql_column = sa.column(target_col.get("name")) + else: + sql_column = target_col + + qry = sa.select([sql_column]).select_from(tbl).distinct() + if limit: + qry = qry.limit(limit) + + engine = self.database.get_sqla_engine() + sql = qry.compile(engine, compile_kwargs={"literal_binds": True}) + sql = self._apply_cte(sql, cte) + sql = self.mutate_query_from_config(sql) + + df = pd.read_sql_query(sql=sql, con=engine) + return df[column_name].to_list() + + def get_timestamp_expression( + self, + column: Dict[str, Any], + time_grain: Optional[str], + label: Optional[str] = None, + template_processor: Optional[ + BaseTemplateProcessor + ] = None, # pylint: disable=unused-argument + ) -> Union[TimestampExpression, Label]: + """ + Return a SQLAlchemy Core element representation of self to be used in a query. + + :param time_grain: Optional time grain, e.g. P1Y + :param label: alias/label that column is expected to have + :param template_processor: template processor + :return: A TimeExpression object wrapped in a Label if supported by db + """ + label = label or utils.DTTM_ALIAS + column_spec = self.db_engine_spec.get_column_spec(column.get("type")) + type_ = column_spec.sqla_type if column_spec else sa.DateTime + col = sa.column(column.get("column_name"), type_=type_) + time_expr = self.db_engine_spec.get_timestamp_expr(col, None, time_grain) + return self.make_sqla_column_compatible(time_expr, label) + def get_sqla_query( # pylint: disable=too-many-arguments,too-many-locals,too-many-branches,too-many-statements self, apply_fetch_values_predicate: bool = False, @@ -1410,9 +1467,14 @@ class ExploreMixin: # pylint: disable=too-many-public-methods time_filters: List[Any] = [] if is_timeseries: - timestamp = dttm_col.get_timestamp_expression( - time_grain=time_grain, template_processor=template_processor - ) + if isinstance(dttm_col, dict): + timestamp = self.get_timestamp_expression( + dttm_col, time_grain, template_processor + ) + else: + timestamp = dttm_col.get_timestamp_expression( + time_grain=time_grain, template_processor=template_processor + ) # always put timestamp as the first column select_exprs.insert(0, timestamp) groupby_all_columns[timestamp.name] = timestamp @@ -1467,9 +1529,15 @@ class ExploreMixin: # pylint: disable=too-many-public-methods if sqla_col is not None: pass elif col_obj and filter_grain: - sqla_col = col_obj.get_timestamp_expression( - time_grain=filter_grain, template_processor=template_processor - ) + if isinstance(col_obj, dict): + sqla_col = self.get_timestamp_expression( + col_obj, time_grain, template_processor + ) + else: + sqla_col = col_obj.get_timestamp_expression( + time_grain=filter_grain, + template_processor=template_processor, + ) elif col_obj and isinstance(col_obj, dict): sqla_col = sa.column(col_obj.get("column_name")) elif col_obj: diff --git a/superset/utils/core.py b/superset/utils/core.py index 44d76ea533..ba7237b77b 100644 --- a/superset/utils/core.py +++ b/superset/utils/core.py @@ -1728,7 +1728,7 @@ def is_test() -> bool: return strtobool(os.environ.get("SUPERSET_TESTENV", "false")) -def get_time_filter_status( # pylint: disable=too-many-branches +def get_time_filter_status( datasource: "BaseDatasource", applied_time_extras: Dict[str, str], ) -> Tuple[List[Dict[str, str]], List[Dict[str, str]]]: diff --git a/superset/views/core.py b/superset/views/core.py index 0598c72446..2b732aad4f 100755 --- a/superset/views/core.py +++ b/superset/views/core.py @@ -427,13 +427,13 @@ class Superset(BaseSupersetView): # pylint: disable=too-many-public-methods _, slc = get_form_data(slice_id, use_slice_data=True) if not slc: abort(404) - endpoint = "/superset/explore/?form_data={}".format( + endpoint = "/explore/?form_data={}".format( parse.quote(json.dumps({"slice_id": slice_id})) ) is_standalone_mode = ReservedUrlParameters.is_standalone_mode() if is_standalone_mode: - endpoint += f"&{ReservedUrlParameters.STANDALONE}={is_standalone_mode}" + endpoint += f"&{ReservedUrlParameters.STANDALONE}=true" return redirect(endpoint) def get_query_string_response(self, viz_obj: BaseViz) -> FlaskResponse: @@ -815,7 +815,7 @@ class Superset(BaseSupersetView): # pylint: disable=too-many-public-methods try: datasource = DatasourceDAO.get_datasource( db.session, - DatasourceType(cast(str, datasource_type)), + DatasourceType("table"), datasource_id, ) except DatasetNotFoundError: @@ -2477,9 +2477,7 @@ class Superset(BaseSupersetView): # pylint: disable=too-many-public-methods @has_access @event_logger.log_this @expose("/csv/<client_id>") - def csv( # pylint: disable=no-self-use,too-many-locals - self, client_id: str - ) -> FlaskResponse: + def csv(self, client_id: str) -> FlaskResponse: # pylint: disable=no-self-use """Download the query results as csv.""" logger.info("Exporting CSV file [%s]", client_id) query = db.session.query(Query).filter_by(client_id=client_id).one() @@ -2502,8 +2500,13 @@ class Superset(BaseSupersetView): # pylint: disable=too-many-public-methods obj = _deserialize_results_payload( payload, query, cast(bool, results_backend_use_msgpack) ) - columns = [c["name"] for c in obj["columns"]] - df = pd.DataFrame.from_records(obj["data"], columns=columns) + + df = pd.DataFrame( + data=obj["data"], + dtype=object, + columns=[c["name"] for c in obj["columns"]], + ) + logger.info("Using pandas to convert to CSV") else: logger.info("Running a query to turn into CSV")
