refactor: migrate ExploreResultsButton component to FC & tsx (#18143)

* Move ExploreResultsButton to FC & tsx

* Refactoring

* Refactoring

* Refactoring

* Refactoring

* Refactoring

* Refactoring

* Refactoring

* Fix test
This commit is contained in:
EugeneTorap 2022-02-08 18:57:08 +03:00 committed by GitHub
parent fa8c81e1b1
commit cdfcbbaf30
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
4 changed files with 61 additions and 329 deletions

View File

@ -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(<ExploreResultsButton store={store} {...props} />)
shallow(
<ThemeProvider theme={supersetTheme}>
<ExploreResultsButton store={store} {...props} />
</ThemeProvider>,
)
.dive()
.dive();
it('renders', () => {
expect(React.isValidElement(<ExploreResultsButton />)).toBe(true);
});
it('renders with props', () => {
expect(
React.isValidElement(<ExploreResultsButton {...mockedProps} />),
).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(
<ExploreResultsButton store={store} {...props} />,
)
.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();
});
});
});

View File

@ -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 (
<Alert
type="warning"
message={
<>
{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 ')}
<strong>CREATE TABLE AS </strong>
{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 (
<div>
{t('Column name(s) ')}
<code>
<strong>{invalidColumns.join(', ')} </strong>
</code>
{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.`)}
</div>
);
}
render() {
const allowsSubquery =
this.props.database && this.props.database.allows_subquery;
return (
<>
<Button
buttonSize="small"
onClick={this.props.onClick}
disabled={!allowsSubquery}
tooltip={t('Explore the result set in the data exploration view')}
>
<InfoTooltipWithTrigger
icon="line-chart"
placement="top"
label="explore"
/>{' '}
{t('Explore')}
</Button>
</>
);
}
}
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);

View File

@ -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 (
<Button
buttonSize="small"
onClick={onClick}
disabled={!allowsSubquery}
tooltip={t('Explore the result set in the data exploration view')}
>
<InfoTooltipWithTrigger
icon="line-chart"
placement="top"
label="explore"
/>{' '}
{t('Explore')}
</Button>
);
};
export default ExploreResultsButton;

View File

@ -527,13 +527,9 @@ export default class ResultSet extends React.PureComponent<
/>
<ResultSetButtons>
{this.props.visualize &&
this.props.database &&
this.props.database.allows_virtual_table_explore && (
this.props.database?.allows_virtual_table_explore && (
<ExploreResultsButton
// @ts-ignore Redux types are difficult to work with, ignoring for now
query={this.props.query}
database={this.props.database}
actions={this.props.actions}
onClick={this.handleExploreBtnClick}
/>
)}