chore: refactor ResultSet to functional component (#21186)

This commit is contained in:
EugeneTorap
2022-09-06 17:55:07 +03:00
committed by GitHub
parent d130b4a24f
commit f603295678
9 changed files with 523 additions and 564 deletions

View File

@@ -1,219 +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 React from 'react';
import { shallow } from 'enzyme';
import { styledMount } from 'spec/helpers/theming';
import { render, screen } from 'spec/helpers/testing-library';
import { Provider } from 'react-redux';
import sinon from 'sinon';
import Alert from 'src/components/Alert';
import ProgressBar from 'src/components/ProgressBar';
import Loading from 'src/components/Loading';
import configureStore from 'redux-mock-store';
import thunk from 'redux-thunk';
import fetchMock from 'fetch-mock';
import FilterableTable from 'src/components/FilterableTable';
import ExploreResultsButton from 'src/SqlLab/components/ExploreResultsButton';
import ResultSet from 'src/SqlLab/components/ResultSet';
import ErrorMessageWithStackTrace from 'src/components/ErrorMessage/ErrorMessageWithStackTrace';
import {
cachedQuery,
failedQueryWithErrorMessage,
failedQueryWithErrors,
queries,
runningQuery,
stoppedQuery,
initialState,
user,
queryWithNoQueryLimit,
} from 'src/SqlLab/fixtures';
const mockStore = configureStore([thunk]);
const store = mockStore(initialState);
const clearQuerySpy = sinon.spy();
const fetchQuerySpy = sinon.spy();
const reRunQuerySpy = sinon.spy();
const mockedProps = {
actions: {
clearQueryResults: clearQuerySpy,
fetchQueryResults: fetchQuerySpy,
reRunQuery: reRunQuerySpy,
},
cache: true,
query: queries[0],
height: 140,
database: { allows_virtual_table_explore: true },
user,
defaultQueryLimit: 1000,
};
const stoppedQueryProps = { ...mockedProps, query: stoppedQuery };
const runningQueryProps = { ...mockedProps, query: runningQuery };
const fetchingQueryProps = {
...mockedProps,
query: {
dbId: 1,
cached: false,
ctas: false,
id: 'ryhHUZCGb',
progress: 100,
state: 'fetching',
startDttm: Date.now() - 500,
},
};
const cachedQueryProps = { ...mockedProps, query: cachedQuery };
const failedQueryWithErrorMessageProps = {
...mockedProps,
query: failedQueryWithErrorMessage,
};
const failedQueryWithErrorsProps = {
...mockedProps,
query: failedQueryWithErrors,
};
const newProps = {
query: {
cached: false,
resultsKey: 'new key',
results: {
data: [{ a: 1 }],
},
},
};
fetchMock.get('glob:*/api/v1/dataset?*', { result: [] });
test('is valid', () => {
expect(React.isValidElement(<ResultSet {...mockedProps} />)).toBe(true);
});
test('renders a Table', () => {
const wrapper = shallow(<ResultSet {...mockedProps} />);
expect(wrapper.find(FilterableTable)).toExist();
});
describe('componentDidMount', () => {
const propsWithError = {
...mockedProps,
query: { ...queries[0], errorMessage: 'Your session timed out' },
};
let spy;
beforeEach(() => {
reRunQuerySpy.resetHistory();
spy = sinon.spy(ResultSet.prototype, 'componentDidMount');
});
afterEach(() => {
spy.restore();
});
it('should call reRunQuery if timed out', () => {
shallow(<ResultSet {...propsWithError} />);
expect(reRunQuerySpy.callCount).toBe(1);
});
it('should not call reRunQuery if no error', () => {
shallow(<ResultSet {...mockedProps} />);
expect(reRunQuerySpy.callCount).toBe(0);
});
});
describe('UNSAFE_componentWillReceiveProps', () => {
const wrapper = shallow(<ResultSet {...mockedProps} />);
let spy;
beforeEach(() => {
clearQuerySpy.resetHistory();
fetchQuerySpy.resetHistory();
spy = sinon.spy(ResultSet.prototype, 'UNSAFE_componentWillReceiveProps');
});
afterEach(() => {
spy.restore();
});
it('should update cached data', () => {
wrapper.setProps(newProps);
expect(wrapper.state().data).toEqual(newProps.query.results.data);
expect(clearQuerySpy.callCount).toBe(1);
expect(clearQuerySpy.getCall(0).args[0]).toEqual(newProps.query);
expect(fetchQuerySpy.callCount).toBe(1);
expect(fetchQuerySpy.getCall(0).args[0]).toEqual(newProps.query);
});
});
test('should render success query', () => {
const wrapper = shallow(<ResultSet {...mockedProps} />);
const filterableTable = wrapper.find(FilterableTable);
expect(filterableTable.props().data).toBe(mockedProps.query.results.data);
expect(wrapper.find(ExploreResultsButton)).toExist();
});
test('should render empty results', () => {
const props = {
...mockedProps,
query: { ...mockedProps.query, results: { data: [] } },
};
const wrapper = styledMount(
<Provider store={store}>
<ResultSet {...props} />
</Provider>,
);
expect(wrapper.find(FilterableTable)).not.toExist();
expect(wrapper.find(Alert)).toExist();
expect(wrapper.find(Alert).render().text()).toBe(
'The query returned no data',
);
});
test('should render cached query', () => {
const wrapper = shallow(<ResultSet {...cachedQueryProps} />);
const cachedData = [{ col1: 'a', col2: 'b' }];
wrapper.setState({ data: cachedData });
const filterableTable = wrapper.find(FilterableTable);
expect(filterableTable.props().data).toBe(cachedData);
});
test('should render stopped query', () => {
const wrapper = shallow(<ResultSet {...stoppedQueryProps} />);
expect(wrapper.find(Alert)).toExist();
});
test('should render running/pending/fetching query', () => {
const wrapper = shallow(<ResultSet {...runningQueryProps} />);
expect(wrapper.find(ProgressBar)).toExist();
});
test('should render fetching w/ 100 progress query', () => {
const wrapper = shallow(<ResultSet {...fetchingQueryProps} />);
expect(wrapper.find(Loading)).toExist();
});
test('should render a failed query with an error message', () => {
const wrapper = shallow(<ResultSet {...failedQueryWithErrorMessageProps} />);
expect(wrapper.find(ErrorMessageWithStackTrace)).toExist();
});
test('should render a failed query with an errors object', () => {
const wrapper = shallow(<ResultSet {...failedQueryWithErrorsProps} />);
expect(wrapper.find(ErrorMessageWithStackTrace)).toExist();
});
test('renders if there is no limit in query.results but has queryLimit', () => {
render(<ResultSet {...mockedProps} />, { useRedux: true });
expect(screen.getByRole('grid')).toBeInTheDocument();
});
test('renders if there is a limit in query.results but not queryLimit', () => {
const props = { ...mockedProps, query: queryWithNoQueryLimit };
render(<ResultSet {...props} />, { useRedux: true });
expect(screen.getByRole('grid')).toBeInTheDocument();
});

View File

@@ -0,0 +1,216 @@
/**
* 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 { render, screen, waitFor } from 'spec/helpers/testing-library';
import configureStore from 'redux-mock-store';
import { Store } from 'redux';
import thunk from 'redux-thunk';
import fetchMock from 'fetch-mock';
import ResultSet from 'src/SqlLab/components/ResultSet';
import {
cachedQuery,
failedQueryWithErrorMessage,
failedQueryWithErrors,
queries,
runningQuery,
stoppedQuery,
initialState,
user,
queryWithNoQueryLimit,
} from 'src/SqlLab/fixtures';
const mockedProps = {
cache: true,
query: queries[0],
height: 140,
database: { allows_virtual_table_explore: true },
user,
defaultQueryLimit: 1000,
};
const stoppedQueryProps = { ...mockedProps, query: stoppedQuery };
const runningQueryProps = { ...mockedProps, query: runningQuery };
const fetchingQueryProps = {
...mockedProps,
query: {
dbId: 1,
cached: false,
ctas: false,
id: 'ryhHUZCGb',
progress: 100,
state: 'fetching',
startDttm: Date.now() - 500,
},
};
const cachedQueryProps = { ...mockedProps, query: cachedQuery };
const failedQueryWithErrorMessageProps = {
...mockedProps,
query: failedQueryWithErrorMessage,
};
const failedQueryWithErrorsProps = {
...mockedProps,
query: failedQueryWithErrors,
};
const newProps = {
query: {
cached: false,
resultsKey: 'new key',
results: {
data: [{ a: 1 }],
},
},
};
fetchMock.get('glob:*/api/v1/dataset?*', { result: [] });
const middlewares = [thunk];
const mockStore = configureStore(middlewares);
const setup = (props?: any, store?: Store) =>
render(<ResultSet {...props} />, {
useRedux: true,
...(store && { store }),
});
describe('ResultSet', () => {
it('renders a Table', async () => {
const { getByTestId } = setup(mockedProps, mockStore(initialState));
const table = getByTestId('table-container');
expect(table).toBeInTheDocument();
});
it('should render success query', async () => {
const { queryAllByText, getByTestId } = setup(
mockedProps,
mockStore(initialState),
);
const table = getByTestId('table-container');
expect(table).toBeInTheDocument();
const firstColumn = queryAllByText(
mockedProps.query.results?.columns[0].name ?? '',
)[0];
const secondColumn = queryAllByText(
mockedProps.query.results?.columns[1].name ?? '',
)[0];
expect(firstColumn).toBeInTheDocument();
expect(secondColumn).toBeInTheDocument();
const exploreButton = getByTestId('explore-results-button');
expect(exploreButton).toBeInTheDocument();
});
it('should render empty results', async () => {
const props = {
...mockedProps,
query: { ...mockedProps.query, results: { data: [] } },
};
await waitFor(() => {
setup(props, mockStore(initialState));
});
const alert = screen.getByRole('alert');
expect(alert).toBeInTheDocument();
expect(alert).toHaveTextContent('The query returned no data');
});
it('should call reRunQuery if timed out', async () => {
const store = mockStore(initialState);
const propsWithError = {
...mockedProps,
query: { ...queries[0], errorMessage: 'Your session timed out' },
};
setup(propsWithError, store);
expect(store.getActions()).toHaveLength(1);
expect(store.getActions()[0].query.errorMessage).toEqual(
'Your session timed out',
);
expect(store.getActions()[0].type).toEqual('START_QUERY');
});
it('should not call reRunQuery if no error', async () => {
const store = mockStore(initialState);
setup(mockedProps, store);
expect(store.getActions()).toEqual([]);
});
it('should render cached query', async () => {
const store = mockStore(initialState);
const { rerender } = setup(cachedQueryProps, store);
// @ts-ignore
rerender(<ResultSet {...newProps} />);
expect(store.getActions()).toHaveLength(1);
expect(store.getActions()[0].query.results).toEqual(
cachedQueryProps.query.results,
);
expect(store.getActions()[0].type).toEqual('CLEAR_QUERY_RESULTS');
});
it('should render stopped query', async () => {
await waitFor(() => {
setup(stoppedQueryProps, mockStore(initialState));
});
const alert = screen.getByRole('alert');
expect(alert).toBeInTheDocument();
});
it('should render running/pending/fetching query', async () => {
const { getByTestId } = setup(runningQueryProps, mockStore(initialState));
const progressBar = getByTestId('progress-bar');
expect(progressBar).toBeInTheDocument();
});
it('should render fetching w/ 100 progress query', async () => {
const { getByRole, getByText } = setup(
fetchingQueryProps,
mockStore(initialState),
);
const loading = getByRole('status');
expect(loading).toBeInTheDocument();
expect(getByText('fetching')).toBeInTheDocument();
});
it('should render a failed query with an error message', async () => {
await waitFor(() => {
setup(failedQueryWithErrorMessageProps, mockStore(initialState));
});
expect(screen.getByText('Database error')).toBeInTheDocument();
expect(screen.getByText('Something went wrong')).toBeInTheDocument();
});
it('should render a failed query with an errors object', async () => {
await waitFor(() => {
setup(failedQueryWithErrorsProps, mockStore(initialState));
});
expect(screen.getByText('Database error')).toBeInTheDocument();
});
it('renders if there is no limit in query.results but has queryLimit', async () => {
const { getByRole } = setup(mockedProps, mockStore(initialState));
expect(getByRole('grid')).toBeInTheDocument();
});
it('renders if there is a limit in query.results but not queryLimit', async () => {
const props = { ...mockedProps, query: queryWithNoQueryLimit };
const { getByRole } = setup(props, mockStore(initialState));
expect(getByRole('grid')).toBeInTheDocument();
});
});

View File

@@ -16,12 +16,14 @@
* specific language governing permissions and limitations
* under the License.
*/
import React from 'react';
import React, { useState, useEffect, useCallback } from 'react';
import { useDispatch } from 'react-redux';
import ButtonGroup from 'src/components/ButtonGroup';
import Alert from 'src/components/Alert';
import Button from 'src/components/Button';
import shortid from 'shortid';
import { styled, t, QueryResponse } from '@superset-ui/core';
import { usePrevious } from 'src/hooks/usePrevious';
import ErrorMessageWithStackTrace from 'src/components/ErrorMessage/ErrorMessageWithStackTrace';
import {
ISaveableDatasource,
@@ -40,7 +42,14 @@ import FilterableTable, {
import CopyToClipboard from 'src/components/CopyToClipboard';
import { addDangerToast } from 'src/components/MessageToasts/actions';
import { prepareCopyToClipboardTabularData } from 'src/utils/common';
import { CtasEnum } from 'src/SqlLab/actions/sqlLab';
import {
CtasEnum,
clearQueryResults,
addQueryEditor,
fetchQueryResults,
reFetchQueryResults,
reRunQuery,
} from 'src/SqlLab/actions/sqlLab';
import { URL_PARAMS } from 'src/constants';
import ExploreCtasResultsButton from '../ExploreCtasResultsButton';
import ExploreResultsButton from '../ExploreResultsButton';
@@ -54,9 +63,7 @@ enum LIMITING_FACTOR {
NOT_LIMITED = 'NOT_LIMITED',
}
interface ResultSetProps {
showControls?: boolean;
actions: Record<string, any>;
export interface ResultSetProps {
cache?: boolean;
csv?: boolean;
database?: Record<string, any>;
@@ -70,17 +77,9 @@ interface ResultSetProps {
defaultQueryLimit: number;
}
interface ResultSetState {
searchText: string;
showExploreResultsButton: boolean;
data: Record<string, any>[];
showSaveDatasetModal: boolean;
alertIsOpen: boolean;
}
const ResultlessStyles = styled.div`
position: relative;
min-height: 100px;
min-height: ${({ theme }) => theme.gridUnit * 25}px;
[role='alert'] {
margin-top: ${({ theme }) => theme.gridUnit * 2}px;
}
@@ -100,8 +99,8 @@ const MonospaceDiv = styled.div`
`;
const ReturnedRows = styled.div`
font-size: 13px;
line-height: 24px;
font-size: ${({ theme }) => theme.typography.sizes.s}px;
line-height: ${({ theme }) => theme.gridUnit * 6}px;
`;
const ResultSetControls = styled.div`
@@ -121,115 +120,84 @@ const LimitMessage = styled.span`
margin-left: ${({ theme }) => theme.gridUnit * 2}px;
`;
export default class ResultSet extends React.PureComponent<
ResultSetProps,
ResultSetState
> {
static defaultProps = {
cache: false,
csv: true,
database: {},
search: true,
showSql: false,
visualize: true,
};
const ResultSet = ({
cache = false,
csv = true,
database = {},
displayLimit,
height,
query,
search = true,
showSql = false,
visualize = true,
user,
defaultQueryLimit,
}: ResultSetProps) => {
const [searchText, setSearchText] = useState('');
const [cachedData, setCachedData] = useState<Record<string, unknown>[]>([]);
const [showSaveDatasetModal, setShowSaveDatasetModal] = useState(false);
const [alertIsOpen, setAlertIsOpen] = useState(false);
constructor(props: ResultSetProps) {
super(props);
this.state = {
searchText: '',
showExploreResultsButton: false,
data: [],
showSaveDatasetModal: false,
alertIsOpen: false,
};
this.changeSearch = this.changeSearch.bind(this);
this.fetchResults = this.fetchResults.bind(this);
this.popSelectStar = this.popSelectStar.bind(this);
this.reFetchQueryResults = this.reFetchQueryResults.bind(this);
this.toggleExploreResultsButton =
this.toggleExploreResultsButton.bind(this);
}
const dispatch = useDispatch();
async componentDidMount() {
// only do this the first time the component is rendered/mounted
this.reRunQueryIfSessionTimeoutErrorOnMount();
}
UNSAFE_componentWillReceiveProps(nextProps: ResultSetProps) {
// when new results comes in, save them locally and clear in store
if (
this.props.cache &&
!nextProps.query.cached &&
nextProps.query.results &&
nextProps.query.results.data &&
nextProps.query.results.data.length > 0
) {
this.setState({ data: nextProps.query.results.data }, () =>
this.clearQueryResults(nextProps.query),
);
}
if (
nextProps.query.resultsKey &&
nextProps.query.resultsKey !== this.props.query.resultsKey
) {
this.fetchResults(nextProps.query);
}
}
calculateAlertRefHeight = (alertElement: HTMLElement | null) => {
if (alertElement) {
this.setState({ alertIsOpen: true });
} else {
this.setState({ alertIsOpen: false });
}
};
clearQueryResults(query: QueryResponse) {
this.props.actions.clearQueryResults(query);
}
popSelectStar(tempSchema: string | null, tempTable: string) {
const qe = {
id: shortid.generate(),
name: tempTable,
autorun: false,
dbId: this.props.query.dbId,
sql: `SELECT * FROM ${tempSchema ? `${tempSchema}.` : ''}${tempTable}`,
};
this.props.actions.addQueryEditor(qe);
}
toggleExploreResultsButton() {
this.setState(prevState => ({
showExploreResultsButton: !prevState.showExploreResultsButton,
}));
}
changeSearch(event: React.ChangeEvent<HTMLInputElement>) {
this.setState({ searchText: event.target.value });
}
fetchResults(query: QueryResponse) {
this.props.actions.fetchQueryResults(query, this.props.displayLimit);
}
reFetchQueryResults(query: QueryResponse) {
this.props.actions.reFetchQueryResults(query);
}
reRunQueryIfSessionTimeoutErrorOnMount() {
const { query } = this.props;
const reRunQueryIfSessionTimeoutErrorOnMount = useCallback(() => {
if (
query.errorMessage &&
query.errorMessage.indexOf('session timed out') > 0
) {
this.props.actions.reRunQuery(query);
dispatch(reRunQuery(query));
}
}
}, []);
createExploreResultsOnClick = async () => {
const { results } = this.props.query;
useEffect(() => {
// only do this the first time the component is rendered/mounted
reRunQueryIfSessionTimeoutErrorOnMount();
}, [reRunQueryIfSessionTimeoutErrorOnMount]);
const fetchResults = (query: QueryResponse) => {
dispatch(fetchQueryResults(query, displayLimit));
};
const prevQuery = usePrevious(query);
useEffect(() => {
if (cache && query.cached && query?.results?.data?.length > 0) {
setCachedData(query.results.data);
dispatch(clearQueryResults(query));
}
if (
query.resultsKey &&
prevQuery?.resultsKey &&
query.resultsKey !== prevQuery.resultsKey
) {
fetchResults(query);
}
}, [query, cache]);
const calculateAlertRefHeight = (alertElement: HTMLElement | null) => {
if (alertElement) {
setAlertIsOpen(true);
} else {
setAlertIsOpen(false);
}
};
const popSelectStar = (tempSchema: string | null, tempTable: string) => {
const qe = {
id: shortid.generate(),
name: tempTable,
autorun: false,
dbId: query.dbId,
sql: `SELECT * FROM ${tempSchema ? `${tempSchema}.` : ''}${tempTable}`,
};
dispatch(addQueryEditor(qe));
};
const changeSearch = (event: React.ChangeEvent<HTMLInputElement>) => {
setSearchText(event.target.value);
};
const createExploreResultsOnClick = async () => {
const { results } = query;
if (results?.query_id) {
const key = await postFormData(results.query_id, 'query', {
@@ -248,16 +216,14 @@ export default class ResultSet extends React.PureComponent<
}
};
renderControls() {
if (this.props.search || this.props.visualize || this.props.csv) {
let { data } = this.props.query.results;
if (this.props.cache && this.props.query.cached) {
({ data } = this.state);
const renderControls = () => {
if (search || visualize || csv) {
let { data } = query.results;
if (cache && query.cached) {
data = cachedData;
}
const { columns } = this.props.query.results;
const { columns } = query.results;
// Added compute logic to stop user from being able to Save & Explore
const { showSaveDatasetModal } = this.state;
const { query } = this.props;
const datasource: ISaveableDatasource = {
columns: query.results.columns as ISimpleColumn[],
@@ -272,7 +238,7 @@ export default class ResultSet extends React.PureComponent<
<ResultSetControls>
<SaveDatasetModal
visible={showSaveDatasetModal}
onHide={() => this.setState({ showSaveDatasetModal: false })}
onHide={() => setShowSaveDatasetModal(false)}
buttonTextOnSave={t('Save & Explore')}
buttonTextOnOverwrite={t('Overwrite & Explore')}
modalDescription={t(
@@ -281,14 +247,13 @@ export default class ResultSet extends React.PureComponent<
datasource={datasource}
/>
<ResultSetButtons>
{this.props.visualize &&
this.props.database?.allows_virtual_table_explore && (
<ExploreResultsButton
database={this.props.database}
onClick={this.createExploreResultsOnClick}
/>
)}
{this.props.csv && (
{visualize && database?.allows_virtual_table_explore && (
<ExploreResultsButton
database={database}
onClick={createExploreResultsOnClick}
/>
)}
{csv && (
<Button buttonSize="small" href={`/superset/csv/${query.id}`}>
<i className="fa fa-file-text-o" /> {t('Download to CSV')}
</Button>
@@ -305,11 +270,11 @@ export default class ResultSet extends React.PureComponent<
hideTooltip
/>
</ResultSetButtons>
{this.props.search && (
{search && (
<input
type="text"
onChange={this.changeSearch}
value={this.state.searchText}
onChange={changeSearch}
value={searchText}
className="form-control input-sm"
disabled={columns.length > MAX_COLUMNS_FOR_TABLE}
placeholder={
@@ -323,14 +288,14 @@ export default class ResultSet extends React.PureComponent<
);
}
return <div />;
}
};
renderRowsReturned() {
const { results, rows, queryLimit, limitingFactor } = this.props.query;
const renderRowsReturned = () => {
const { results, rows, queryLimit, limitingFactor } = query;
let limitMessage;
const limitReached = results?.displayLimitReached;
const limit = queryLimit || results.query.limit;
const isAdmin = !!this.props.user?.roles?.Admin;
const isAdmin = !!user?.roles?.Admin;
const rowsCount = Math.min(rows || 0, results?.data?.length || 0);
const displayMaxRowsReachedMessage = {
@@ -348,10 +313,10 @@ export default class ResultSet extends React.PureComponent<
),
};
const shouldUseDefaultDropdownAlert =
limit === this.props.defaultQueryLimit &&
limit === defaultQueryLimit &&
limitingFactor === LIMITING_FACTOR.DROPDOWN;
if (limitingFactor === LIMITING_FACTOR.QUERY && this.props.csv) {
if (limitingFactor === LIMITING_FACTOR.QUERY && csv) {
limitMessage = t(
'The number of rows displayed is limited to %(rows)d by the query',
{ rows },
@@ -386,11 +351,11 @@ export default class ResultSet extends React.PureComponent<
</span>
)}
{!limitReached && shouldUseDefaultDropdownAlert && (
<div ref={this.calculateAlertRefHeight}>
<div ref={calculateAlertRefHeight}>
<Alert
type="warning"
message={t('%(rows)d rows returned', { rows })}
onClose={() => this.setState({ alertIsOpen: false })}
onClose={() => setAlertIsOpen(false)}
description={t(
'The number of rows displayed is limited to %s by the dropdown.',
rows,
@@ -399,10 +364,10 @@ export default class ResultSet extends React.PureComponent<
</div>
)}
{limitReached && (
<div ref={this.calculateAlertRefHeight}>
<div ref={calculateAlertRefHeight}>
<Alert
type="warning"
onClose={() => this.setState({ alertIsOpen: false })}
onClose={() => setAlertIsOpen(false)}
message={t('%(rows)d rows returned', { rows: rowsCount })}
description={
isAdmin
@@ -414,193 +379,191 @@ export default class ResultSet extends React.PureComponent<
)}
</ReturnedRows>
);
};
const limitReached = query?.results?.displayLimitReached;
let sql;
let exploreDBId = query.dbId;
if (database?.explore_database_id) {
exploreDBId = database.explore_database_id;
}
render() {
const { query } = this.props;
const limitReached = query?.results?.displayLimitReached;
let sql;
let exploreDBId = query.dbId;
if (this.props.database && this.props.database.explore_database_id) {
exploreDBId = this.props.database.explore_database_id;
}
let trackingUrl;
if (
query.trackingUrl &&
query.state !== 'success' &&
query.state !== 'fetching'
) {
trackingUrl = (
<Button
className="sql-result-track-job"
buttonSize="small"
href={query.trackingUrl}
target="_blank"
>
{query.state === 'running' ? t('Track job') : t('See query details')}
</Button>
);
}
let trackingUrl;
if (
query.trackingUrl &&
query.state !== 'success' &&
query.state !== 'fetching'
) {
trackingUrl = (
<Button
className="sql-result-track-job"
buttonSize="small"
href={query.trackingUrl}
target="_blank"
>
{query.state === 'running' ? t('Track job') : t('See query details')}
</Button>
);
}
if (this.props.showSql) sql = <HighlightedSql sql={query.sql} />;
if (showSql) {
sql = <HighlightedSql sql={query.sql} />;
}
if (query.state === 'stopped') {
return <Alert type="warning" message={t('Query was stopped')} />;
}
if (query.state === 'failed') {
return (
<ResultlessStyles>
<ErrorMessageWithStackTrace
title={t('Database error')}
error={query?.errors?.[0]}
subtitle={<MonospaceDiv>{query.errorMessage}</MonospaceDiv>}
copyText={query.errorMessage || undefined}
link={query.link}
source="sqllab"
/>
{trackingUrl}
</ResultlessStyles>
);
}
if (query.state === 'success' && query.ctas) {
const { tempSchema, tempTable } = query;
let object = 'Table';
if (query.ctas_method === CtasEnum.VIEW) {
object = 'View';
}
return (
<div>
<Alert
type="info"
message={
<>
{t(object)} [
<strong>
{tempSchema ? `${tempSchema}.` : ''}
{tempTable}
</strong>
] {t('was created')} &nbsp;
<ButtonGroup>
<Button
buttonSize="small"
className="m-r-5"
onClick={() => this.popSelectStar(tempSchema, tempTable)}
>
{t('Query in a new tab')}
</Button>
<ExploreCtasResultsButton
// @ts-ignore Redux types are difficult to work with, ignoring for now
actions={this.props.actions}
table={tempTable}
schema={tempSchema}
dbId={exploreDBId}
/>
</ButtonGroup>
</>
}
/>
</div>
);
}
if (query.state === 'success' && query.results) {
const { results } = query;
// Accounts for offset needed for height of ResultSetRowsReturned component if !limitReached
const rowMessageHeight = !limitReached ? 32 : 0;
// Accounts for offset needed for height of Alert if this.state.alertIsOpen
const alertContainerHeight = 70;
// We need to calculate the height of this.renderRowsReturned()
// if we want results panel to be propper height because the
// FilterTable component nedds an explcit height to render
// react-virtualized Table component
const height = this.state.alertIsOpen
? this.props.height - alertContainerHeight
: this.props.height - rowMessageHeight;
let data;
if (this.props.cache && query.cached) {
({ data } = this.state);
} else if (results && results.data) {
({ data } = results);
}
if (data && data.length > 0) {
const expandedColumns = results.expanded_columns
? results.expanded_columns.map(col => col.name)
: [];
return (
<>
{this.renderControls()}
{this.renderRowsReturned()}
{sql}
<FilterableTable
data={data}
orderedColumnKeys={results.columns.map(col => col.name)}
height={height}
filterText={this.state.searchText}
expandedColumns={expandedColumns}
/>
</>
);
}
if (data && data.length === 0) {
return (
<Alert type="warning" message={t('The query returned no data')} />
);
}
}
if (query.cached || (query.state === 'success' && !query.results)) {
if (query.isDataPreview) {
return (
<Button
buttonSize="small"
buttonStyle="primary"
onClick={() =>
this.reFetchQueryResults({
...query,
isDataPreview: true,
})
}
>
{t('Fetch data preview')}
</Button>
);
}
if (query.resultsKey) {
return (
<Button
buttonSize="small"
buttonStyle="primary"
onClick={() => this.fetchResults(query)}
>
{t('Refetch results')}
</Button>
);
}
}
let progressBar;
if (query.progress > 0) {
progressBar = (
<ProgressBar
percent={parseInt(query.progress.toFixed(0), 10)}
striped
/>
);
}
const progressMsg =
query && query.extra && query.extra.progress
? query.extra.progress
: null;
if (query.state === 'stopped') {
return <Alert type="warning" message={t('Query was stopped')} />;
}
if (query.state === 'failed') {
return (
<ResultlessStyles>
<div>{!progressBar && <Loading position="normal" />}</div>
{/* show loading bar whenever progress bar is completed but needs time to render */}
<div>{query.progress === 100 && <Loading position="normal" />}</div>
<QueryStateLabel query={query} />
<div>
{progressMsg && <Alert type="success" message={progressMsg} />}
</div>
<div>{query.progress !== 100 && progressBar}</div>
{trackingUrl && <div>{trackingUrl}</div>}
<ErrorMessageWithStackTrace
title={t('Database error')}
error={query?.errors?.[0]}
subtitle={<MonospaceDiv>{query.errorMessage}</MonospaceDiv>}
copyText={query.errorMessage || undefined}
link={query.link}
source="sqllab"
/>
{trackingUrl}
</ResultlessStyles>
);
}
}
if (query.state === 'success' && query.ctas) {
const { tempSchema, tempTable } = query;
let object = 'Table';
if (query.ctas_method === CtasEnum.VIEW) {
object = 'View';
}
return (
<div>
<Alert
type="info"
message={
<>
{t(object)} [
<strong>
{tempSchema ? `${tempSchema}.` : ''}
{tempTable}
</strong>
] {t('was created')} &nbsp;
<ButtonGroup>
<Button
buttonSize="small"
className="m-r-5"
onClick={() => popSelectStar(tempSchema, tempTable)}
>
{t('Query in a new tab')}
</Button>
<ExploreCtasResultsButton
table={tempTable}
schema={tempSchema}
dbId={exploreDBId}
/>
</ButtonGroup>
</>
}
/>
</div>
);
}
if (query.state === 'success' && query.results) {
const { results } = query;
// Accounts for offset needed for height of ResultSetRowsReturned component if !limitReached
const rowMessageHeight = !limitReached ? 32 : 0;
// Accounts for offset needed for height of Alert if this.state.alertIsOpen
const alertContainerHeight = 70;
// We need to calculate the height of this.renderRowsReturned()
// if we want results panel to be proper height because the
// FilterTable component needs an explicit height to render
// react-virtualized Table component
const rowsHeight = alertIsOpen
? height - alertContainerHeight
: height - rowMessageHeight;
let data;
if (cache && query.cached) {
data = cachedData;
} else if (results?.data) {
({ data } = results);
}
if (data && data.length > 0) {
const expandedColumns = results.expanded_columns
? results.expanded_columns.map(col => col.name)
: [];
return (
<>
{renderControls()}
{renderRowsReturned()}
{sql}
<FilterableTable
data={data}
orderedColumnKeys={results.columns.map(col => col.name)}
height={rowsHeight}
filterText={searchText}
expandedColumns={expandedColumns}
/>
</>
);
}
if (data && data.length === 0) {
return <Alert type="warning" message={t('The query returned no data')} />;
}
}
if (query.cached || (query.state === 'success' && !query.results)) {
if (query.isDataPreview) {
return (
<Button
buttonSize="small"
buttonStyle="primary"
onClick={() =>
dispatch(
reFetchQueryResults({
...query,
isDataPreview: true,
}),
)
}
>
{t('Fetch data preview')}
</Button>
);
}
if (query.resultsKey) {
return (
<Button
buttonSize="small"
buttonStyle="primary"
onClick={() => fetchResults(query)}
>
{t('Refetch results')}
</Button>
);
}
}
let progressBar;
if (query.progress > 0) {
progressBar = (
<ProgressBar percent={parseInt(query.progress.toFixed(0), 10)} striped />
);
}
const progressMsg = query?.extra?.progress ?? null;
return (
<ResultlessStyles>
<div>{!progressBar && <Loading position="normal" />}</div>
{/* show loading bar whenever progress bar is completed but needs time to render */}
<div>{query.progress === 100 && <Loading position="normal" />}</div>
<QueryStateLabel query={query} />
<div>{progressMsg && <Alert type="success" message={progressMsg} />}</div>
<div>{query.progress !== 100 && progressBar}</div>
{trackingUrl && <div>{trackingUrl}</div>}
</ResultlessStyles>
);
};
export default ResultSet;