mirror of https://github.com/apache/superset.git
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:
parent
fa8c81e1b1
commit
cdfcbbaf30
|
@ -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();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -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);
|
|
@ -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;
|
|
@ -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}
|
||||
/>
|
||||
)}
|
||||
|
|
Loading…
Reference in New Issue