/** * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ import moment from 'moment'; import React from 'react'; import PropTypes from 'prop-types'; import { bindActionCreators } from 'redux'; import { connect } from 'react-redux'; import { Alert } from 'react-bootstrap'; import Dialog from 'react-bootstrap-dialog'; import { t } from '@superset-ui/translation'; import { InfoTooltipWithTrigger } from '@superset-ui/control-utils'; import shortid from 'shortid'; import { exploreChart } from '../../explore/exploreUtils'; import * as actions from '../actions/sqlLab'; import Button from '../../components/Button'; const propTypes = { actions: PropTypes.object.isRequired, query: PropTypes.object, errorMessage: PropTypes.string, timeout: PropTypes.number, database: PropTypes.object.isRequired, }; const defaultProps = { query: {}, }; class ExploreResultsButton extends React.PureComponent { constructor(props) { super(props); this.visualize = this.visualize.bind(this); this.onClick = this.onClick.bind(this); this.getInvalidColumns = this.getInvalidColumns.bind(this); this.renderInvalidColumnMessage = this.renderInvalidColumnMessage.bind( this, ); } onClick() { const timeout = this.props.timeout; const msg = this.renderInvalidColumnMessage(); if (Math.round(this.getQueryDuration()) > timeout) { this.dialog.show({ title: t('Explore'), body: this.renderTimeoutWarning(), actions: [ Dialog.CancelAction(), Dialog.OKAction(() => { this.visualize(); }), ], bsSize: 'large', onHide: dialog => { dialog.hide(); }, }); } else if (msg) { this.dialog.show({ title: t('Explore'), body: msg, actions: [Dialog.DefaultAction('Ok', () => {}, 'btn-primary')], bsSize: 'large', bsStyle: 'warning', onHide: dialog => { dialog.hide(); }, }); } else { this.visualize(); } } getColumns() { const props = this.props; if ( props.query && props.query.results && props.query.results.selected_columns ) { return props.query.results.selected_columns; } return []; } getQueryDuration() { return moment .duration(this.props.query.endDttm - this.props.query.startDttm) .asSeconds(); } getInvalidColumns() { const re1 = /^[A-Za-z_]\w*$/; // starts with char or _, then only alphanum const re2 = /__\d+$/; // does not finish with __ and then a number which screams dup col name return this.props.query.results.selected_columns .map(col => col.name) .filter(col => !re1.test(col) || re2.test(col)); } datasourceName() { const { query } = this.props; const uniqueId = shortid.generate(); let datasourceName = uniqueId; if (query) { datasourceName = query.user ? `${query.user}-` : ''; datasourceName += `${query.tab}-${uniqueId}`; } return datasourceName; } buildVizOptions() { const { schema, sql, dbId, templateParams } = this.props.query; return { dbId, schema, sql, templateParams, datasourceName: this.datasourceName(), columns: this.getColumns(), }; } visualize() { this.props.actions .createDatasource(this.buildVizOptions()) .then(data => { const columns = this.getColumns(); const formData = { datasource: `${data.table_id}__table`, metrics: [], groupby: [], time_range: 'No filter', viz_type: 'table', all_columns: columns.map(c => c.name), row_limit: 1000, }; this.props.actions.addInfoToast( t('Creating a data source and creating a new tab'), ); // open new window for data visualization exploreChart(formData); }) .catch(() => { this.props.actions.addDangerToast( this.props.errorMessage || t('An error occurred'), ); }); } renderTimeoutWarning() { return ( {t( 'This query took %s seconds to run, ', Math.round(this.getQueryDuration()), ) + t( 'and the explore view times out at %s seconds ', this.props.timeout, ) + t( 'following this flow will most likely lead to your query timing out. ', ) + t( 'We recommend your summarize your data further before following that flow. ', ) + t('If activated you can use the ')} CREATE TABLE AS {t('feature to store a summarized data set that you can then explore.')} ); } renderInvalidColumnMessage() { const invalidColumns = this.getInvalidColumns(); if (invalidColumns.length === 0) { return null; } return (
{t('Column name(s) ')} {invalidColumns.join(', ')} {t('cannot be used as a column name. Please use aliases (as in ')} SELECT count(*) AS my_alias ){' '} {t(`limited to alphanumeric characters and underscores. Column aliases ending with double underscores followed by a numeric value are not allowed for reasons discussed in Github issue #5739. `)}
); } render() { const allowsSubquery = this.props.database && this.props.database.allows_subquery; return ( <> { this.dialog = el; }} /> ); } } ExploreResultsButton.propTypes = propTypes; ExploreResultsButton.defaultProps = defaultProps; function mapStateToProps({ sqlLab, common }) { return { errorMessage: sqlLab.errorMessage, timeout: common.conf ? common.conf.SUPERSET_WEBSERVER_TIMEOUT : null, }; } function mapDispatchToProps(dispatch) { return { actions: bindActionCreators(actions, dispatch), }; } export { ExploreResultsButton }; export default connect( mapStateToProps, mapDispatchToProps, )(ExploreResultsButton);