diff --git a/superset-frontend/src/SqlLab/components/ExploreResultsButton/ExploreResultsButton.test.jsx b/superset-frontend/src/SqlLab/components/ExploreResultsButton/ExploreResultsButton.test.jsx index b524235dc290a..e9f1740517e0f 100644 --- a/superset-frontend/src/SqlLab/components/ExploreResultsButton/ExploreResultsButton.test.jsx +++ b/superset-frontend/src/SqlLab/components/ExploreResultsButton/ExploreResultsButton.test.jsx @@ -20,14 +20,10 @@ import React from 'react'; import configureStore from 'redux-mock-store'; import thunk from 'redux-thunk'; import { shallow } from 'enzyme'; -import sinon from 'sinon'; -import fetchMock from 'fetch-mock'; -import shortid from 'shortid'; import sqlLabReducer from 'src/SqlLab/reducers/index'; import ExploreResultsButton from 'src/SqlLab/components/ExploreResultsButton'; -import * as exploreUtils from 'src/explore/exploreUtils'; import Button from 'src/components/Button'; -import { queries, queryWithBadColumns } from 'src/SqlLab/fixtures'; +import { supersetTheme, ThemeProvider } from '@superset-ui/core'; describe('ExploreResultsButton', () => { const middlewares = [thunk]; @@ -46,146 +42,26 @@ describe('ExploreResultsButton', () => { const store = mockStore(initialState); const mockedProps = { database, - show: true, - query: queries[0], onClick() {}, }; - const mockColumns = { - ds: { - is_date: true, - name: 'ds', - type: 'STRING', - }, - gender: { - is_date: false, - name: 'gender', - type: 'STRING', - }, - }; - const mockChartTypeBarChart = { - label: 'Distribution - Bar Chart', - value: 'dist_bar', - }; - const mockChartTypeTB = { - label: 'Time Series - Bar Chart', - value: 'bar', - }; + const getExploreResultsButtonWrapper = (props = mockedProps) => - shallow() + shallow( + + + , + ) .dive() .dive(); - it('renders', () => { - expect(React.isValidElement()).toBe(true); - }); - it('renders with props', () => { expect( React.isValidElement(), ).toBe(true); }); - it('detects bad columns', () => { - const wrapper = getExploreResultsButtonWrapper({ - database, - show: true, - query: queryWithBadColumns, - onClick() {}, - }); - - const badCols = wrapper.instance().getInvalidColumns(); - expect(badCols).toEqual(['my_dupe_col__2', '__timestamp', '__TIMESTAMP']); - - const msgWrapper = shallow(wrapper.instance().renderInvalidColumnMessage()); - expect(msgWrapper.find('div')).toHaveLength(1); - }); - it('renders a Button', () => { const wrapper = getExploreResultsButtonWrapper(); expect(wrapper.find(Button)).toExist(); }); - - describe('datasourceName', () => { - let wrapper; - let stub; - beforeEach(() => { - wrapper = getExploreResultsButtonWrapper(); - stub = sinon.stub(shortid, 'generate').returns('abcd'); - }); - afterEach(() => { - stub.restore(); - }); - - it('should generate data source name from query', () => { - const sampleQuery = queries[0]; - const name = wrapper.instance().datasourceName(); - expect(name).toBe(`${sampleQuery.user}-${sampleQuery.tab}-abcd`); - }); - it('should generate data source name with empty query', () => { - wrapper.setProps({ query: {} }); - const name = wrapper.instance().datasourceName(); - expect(name).toBe('undefined-abcd'); - }); - - it('should build viz options', () => { - wrapper.setState({ chartType: mockChartTypeTB }); - const spy = sinon.spy(wrapper.instance(), 'buildVizOptions'); - wrapper.instance().buildVizOptions(); - expect(spy.returnValues[0]).toEqual({ - schema: 'test_schema', - sql: wrapper.instance().props.query.sql, - dbId: wrapper.instance().props.query.dbId, - columns: Object.values(mockColumns), - templateParams: undefined, - datasourceName: 'admin-Demo-abcd', - }); - }); - }); - - it('should build visualize advise for long query', () => { - const longQuery = { ...queries[0], endDttm: 1476910666798 }; - const props = { - show: true, - query: longQuery, - database, - onClick() {}, - }; - const longQueryWrapper = shallow( - , - ) - .dive() - .dive(); - const inst = longQueryWrapper.instance(); - expect(inst.getQueryDuration()).toBe(100.7050400390625); - }); - - describe('visualize', () => { - const wrapper = getExploreResultsButtonWrapper(); - const mockOptions = { attr: 'mockOptions' }; - wrapper.setState({ - chartType: mockChartTypeBarChart, - datasourceName: 'mockDatasourceName', - }); - - const visualizeURL = '/superset/sqllab_viz/'; - const visualizeEndpoint = `glob:*${visualizeURL}`; - const visualizationPayload = { table_id: 107 }; - fetchMock.post(visualizeEndpoint, visualizationPayload); - - beforeEach(() => { - sinon.stub(exploreUtils, 'getExploreUrl').callsFake(() => 'mockURL'); - sinon.spy(exploreUtils, 'exportChart'); - sinon.spy(exploreUtils, 'exploreChart'); - sinon - .stub(wrapper.instance(), 'buildVizOptions') - .callsFake(() => mockOptions); - }); - afterEach(() => { - exploreUtils.getExploreUrl.restore(); - exploreUtils.exploreChart.restore(); - exploreUtils.exportChart.restore(); - wrapper.instance().buildVizOptions.restore(); - fetchMock.reset(); - }); - }); }); diff --git a/superset-frontend/src/SqlLab/components/ExploreResultsButton/index.jsx b/superset-frontend/src/SqlLab/components/ExploreResultsButton/index.jsx deleted file mode 100644 index 969a7255a2340..0000000000000 --- a/superset-frontend/src/SqlLab/components/ExploreResultsButton/index.jsx +++ /dev/null @@ -1,193 +0,0 @@ -/** - * 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 'src/components/Alert'; -import { t } from '@superset-ui/core'; -import { InfoTooltipWithTrigger } from '@superset-ui/chart-controls'; -import shortid from 'shortid'; -import Button from 'src/components/Button'; -import * as actions from 'src/SqlLab/actions/sqlLab'; - -const propTypes = { - actions: PropTypes.object.isRequired, - query: PropTypes.object, - errorMessage: PropTypes.string, - timeout: PropTypes.number, - database: PropTypes.object.isRequired, - onClick: PropTypes.func.isRequired, -}; -const defaultProps = { - query: {}, -}; - -class ExploreResultsButton extends React.PureComponent { - constructor(props) { - super(props); - this.getInvalidColumns = this.getInvalidColumns.bind(this); - this.renderInvalidColumnMessage = - this.renderInvalidColumnMessage.bind(this); - } - - getColumns() { - const { props } = this; - 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 = /__\d+$/; // duplicate column name pattern - const re2 = /^__timestamp/i; // reserved temporal column alias - - 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(), - }; - } - - 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. The column name/alias "__timestamp" - is reserved for the main temporal expression, and column aliases ending with - double underscores followed by a numeric value (e.g. "my_col__1") are reserved - for deduplicating duplicate column names. Please use aliases to rename the - invalid column names.`)} -
- ); - } - - render() { - const allowsSubquery = - this.props.database && this.props.database.allows_subquery; - return ( - <> - - - ); - } -} -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 default connect( - mapStateToProps, - mapDispatchToProps, -)(ExploreResultsButton); diff --git a/superset-frontend/src/SqlLab/components/ExploreResultsButton/index.tsx b/superset-frontend/src/SqlLab/components/ExploreResultsButton/index.tsx new file mode 100644 index 0000000000000..7c19a4d3b4afe --- /dev/null +++ b/superset-frontend/src/SqlLab/components/ExploreResultsButton/index.tsx @@ -0,0 +1,53 @@ +/** + * 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 React from 'react'; +import { t } from '@superset-ui/core'; +import { InfoTooltipWithTrigger } from '@superset-ui/chart-controls'; +import Button, { OnClickHandler } from 'src/components/Button'; + +interface ExploreResultsButtonProps { + database?: { + allows_subquery?: boolean; + }; + onClick: OnClickHandler; +} + +const ExploreResultsButton = ({ + database, + onClick, +}: ExploreResultsButtonProps) => { + const allowsSubquery = database?.allows_subquery ?? false; + return ( + + ); +}; + +export default ExploreResultsButton; diff --git a/superset-frontend/src/SqlLab/components/ResultSet/index.tsx b/superset-frontend/src/SqlLab/components/ResultSet/index.tsx index a341e82e9f33a..f015b56070a75 100644 --- a/superset-frontend/src/SqlLab/components/ResultSet/index.tsx +++ b/superset-frontend/src/SqlLab/components/ResultSet/index.tsx @@ -527,13 +527,9 @@ export default class ResultSet extends React.PureComponent< /> {this.props.visualize && - this.props.database && - this.props.database.allows_virtual_table_explore && ( + this.props.database?.allows_virtual_table_explore && ( )}