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 configureStore from 'redux-mock-store';
|
||||||
import thunk from 'redux-thunk';
|
import thunk from 'redux-thunk';
|
||||||
import { shallow } from 'enzyme';
|
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 sqlLabReducer from 'src/SqlLab/reducers/index';
|
||||||
import ExploreResultsButton from 'src/SqlLab/components/ExploreResultsButton';
|
import ExploreResultsButton from 'src/SqlLab/components/ExploreResultsButton';
|
||||||
import * as exploreUtils from 'src/explore/exploreUtils';
|
|
||||||
import Button from 'src/components/Button';
|
import Button from 'src/components/Button';
|
||||||
import { queries, queryWithBadColumns } from 'src/SqlLab/fixtures';
|
import { supersetTheme, ThemeProvider } from '@superset-ui/core';
|
||||||
|
|
||||||
describe('ExploreResultsButton', () => {
|
describe('ExploreResultsButton', () => {
|
||||||
const middlewares = [thunk];
|
const middlewares = [thunk];
|
||||||
|
@ -46,146 +42,26 @@ describe('ExploreResultsButton', () => {
|
||||||
const store = mockStore(initialState);
|
const store = mockStore(initialState);
|
||||||
const mockedProps = {
|
const mockedProps = {
|
||||||
database,
|
database,
|
||||||
show: true,
|
|
||||||
query: queries[0],
|
|
||||||
onClick() {},
|
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) =>
|
const getExploreResultsButtonWrapper = (props = mockedProps) =>
|
||||||
shallow(<ExploreResultsButton store={store} {...props} />)
|
shallow(
|
||||||
|
<ThemeProvider theme={supersetTheme}>
|
||||||
|
<ExploreResultsButton store={store} {...props} />
|
||||||
|
</ThemeProvider>,
|
||||||
|
)
|
||||||
.dive()
|
.dive()
|
||||||
.dive();
|
.dive();
|
||||||
|
|
||||||
it('renders', () => {
|
|
||||||
expect(React.isValidElement(<ExploreResultsButton />)).toBe(true);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('renders with props', () => {
|
it('renders with props', () => {
|
||||||
expect(
|
expect(
|
||||||
React.isValidElement(<ExploreResultsButton {...mockedProps} />),
|
React.isValidElement(<ExploreResultsButton {...mockedProps} />),
|
||||||
).toBe(true);
|
).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', () => {
|
it('renders a Button', () => {
|
||||||
const wrapper = getExploreResultsButtonWrapper();
|
const wrapper = getExploreResultsButtonWrapper();
|
||||||
expect(wrapper.find(Button)).toExist();
|
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>
|
<ResultSetButtons>
|
||||||
{this.props.visualize &&
|
{this.props.visualize &&
|
||||||
this.props.database &&
|
this.props.database?.allows_virtual_table_explore && (
|
||||||
this.props.database.allows_virtual_table_explore && (
|
|
||||||
<ExploreResultsButton
|
<ExploreResultsButton
|
||||||
// @ts-ignore Redux types are difficult to work with, ignoring for now
|
|
||||||
query={this.props.query}
|
|
||||||
database={this.props.database}
|
database={this.props.database}
|
||||||
actions={this.props.actions}
|
|
||||||
onClick={this.handleExploreBtnClick}
|
onClick={this.handleExploreBtnClick}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
|
Loading…
Reference in New Issue