You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@superset.apache.org by hu...@apache.org on 2022/10/07 17:14:18 UTC
[superset] 09/13: Revert "chore: refactor ResultSet to functional component (#21186)"
This is an automated email from the ASF dual-hosted git repository.
hugh pushed a commit to branch 2022.39.1
in repository https://gitbox.apache.org/repos/asf/superset.git
commit a169ed12258694200452482da59273c7adcf8070
Author: Joe Li <jo...@preset.io>
AuthorDate: Tue Oct 4 10:41:02 2022 -0700
Revert "chore: refactor ResultSet to functional component (#21186)"
This reverts commit f6032956780380bdac1e3ae950687ae53eabf2e1.
---
.../components/ExploreCtasResultsButton/index.tsx | 25 +-
.../components/ExploreResultsButton/index.tsx | 1 -
.../src/SqlLab/components/QueryTable/index.tsx | 1 +
.../SqlLab/components/ResultSet/ResultSet.test.jsx | 219 ++++++++
.../SqlLab/components/ResultSet/ResultSet.test.tsx | 216 --------
.../src/SqlLab/components/ResultSet/index.tsx | 573 +++++++++++----------
.../src/SqlLab/components/SouthPane/index.tsx | 3 +
.../src/components/FilterableTable/index.tsx | 3 +-
.../src/components/ProgressBar/index.tsx | 2 +-
9 files changed, 542 insertions(+), 501 deletions(-)
diff --git a/superset-frontend/src/SqlLab/components/ExploreCtasResultsButton/index.tsx b/superset-frontend/src/SqlLab/components/ExploreCtasResultsButton/index.tsx
index ac9e8b2fb4..fbcdc15bc5 100644
--- a/superset-frontend/src/SqlLab/components/ExploreCtasResultsButton/index.tsx
+++ b/superset-frontend/src/SqlLab/components/ExploreCtasResultsButton/index.tsx
@@ -17,19 +17,19 @@
* under the License.
*/
import React from 'react';
-import { useSelector, useDispatch } from 'react-redux';
-import { t, JsonObject } from '@superset-ui/core';
-import {
- createCtasDatasource,
- addInfoToast,
- addDangerToast,
-} from 'src/SqlLab/actions/sqlLab';
+import { useSelector } from 'react-redux';
+import { t } from '@superset-ui/core';
import { InfoTooltipWithTrigger } from '@superset-ui/chart-controls';
import Button from 'src/components/Button';
import { exploreChart } from 'src/explore/exploreUtils';
import { SqlLabRootState } from 'src/SqlLab/types';
interface ExploreCtasResultsButtonProps {
+ actions: {
+ createCtasDatasource: Function;
+ addInfoToast: Function;
+ addDangerToast: Function;
+ };
table: string;
schema?: string | null;
dbId: number;
@@ -37,15 +37,16 @@ interface ExploreCtasResultsButtonProps {
}
const ExploreCtasResultsButton = ({
+ actions,
table,
schema,
dbId,
templateParams,
}: ExploreCtasResultsButtonProps) => {
+ const { createCtasDatasource, addInfoToast, addDangerToast } = actions;
const errorMessage = useSelector(
(state: SqlLabRootState) => state.sqlLab.errorMessage,
);
- const dispatch = useDispatch<(dispatch: any) => Promise<JsonObject>>();
const buildVizOptions = {
datasourceName: table,
@@ -55,7 +56,7 @@ const ExploreCtasResultsButton = ({
};
const visualize = () => {
- dispatch(createCtasDatasource(buildVizOptions))
+ createCtasDatasource(buildVizOptions)
.then((data: { table_id: number }) => {
const formData = {
datasource: `${data.table_id}__table`,
@@ -66,14 +67,12 @@ const ExploreCtasResultsButton = ({
all_columns: [],
row_limit: 1000,
};
- dispatch(
- addInfoToast(t('Creating a data source and creating a new tab')),
- );
+ addInfoToast(t('Creating a data source and creating a new tab'));
// open new window for data visualization
exploreChart(formData);
})
.catch(() => {
- dispatch(addDangerToast(errorMessage || t('An error occurred')));
+ addDangerToast(errorMessage || t('An error occurred'));
});
};
diff --git a/superset-frontend/src/SqlLab/components/ExploreResultsButton/index.tsx b/superset-frontend/src/SqlLab/components/ExploreResultsButton/index.tsx
index 4ab7777736..24d5e8686f 100644
--- a/superset-frontend/src/SqlLab/components/ExploreResultsButton/index.tsx
+++ b/superset-frontend/src/SqlLab/components/ExploreResultsButton/index.tsx
@@ -39,7 +39,6 @@ const ExploreResultsButton = ({
onClick={onClick}
disabled={!allowsSubquery}
tooltip={t('Explore the result set in the data exploration view')}
- data-test="explore-results-button"
>
<InfoTooltipWithTrigger
icon="line-chart"
diff --git a/superset-frontend/src/SqlLab/components/QueryTable/index.tsx b/superset-frontend/src/SqlLab/components/QueryTable/index.tsx
index d7ef5ed51c..54edb7f97e 100644
--- a/superset-frontend/src/SqlLab/components/QueryTable/index.tsx
+++ b/superset-frontend/src/SqlLab/components/QueryTable/index.tsx
@@ -245,6 +245,7 @@ const QueryTable = ({
showSql
user={user}
query={query}
+ actions={actions}
height={400}
displayLimit={displayLimit}
defaultQueryLimit={1000}
diff --git a/superset-frontend/src/SqlLab/components/ResultSet/ResultSet.test.jsx b/superset-frontend/src/SqlLab/components/ResultSet/ResultSet.test.jsx
new file mode 100644
index 0000000000..c04e236133
--- /dev/null
+++ b/superset-frontend/src/SqlLab/components/ResultSet/ResultSet.test.jsx
@@ -0,0 +1,219 @@
+/**
+ * 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();
+});
diff --git a/superset-frontend/src/SqlLab/components/ResultSet/ResultSet.test.tsx b/superset-frontend/src/SqlLab/components/ResultSet/ResultSet.test.tsx
deleted file mode 100644
index 2818ed279c..0000000000
--- a/superset-frontend/src/SqlLab/components/ResultSet/ResultSet.test.tsx
+++ /dev/null
@@ -1,216 +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 { 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();
- });
-});
diff --git a/superset-frontend/src/SqlLab/components/ResultSet/index.tsx b/superset-frontend/src/SqlLab/components/ResultSet/index.tsx
index 78387f6dc4..27913d7fc4 100644
--- a/superset-frontend/src/SqlLab/components/ResultSet/index.tsx
+++ b/superset-frontend/src/SqlLab/components/ResultSet/index.tsx
@@ -16,14 +16,12 @@
* specific language governing permissions and limitations
* under the License.
*/
-import React, { useState, useEffect, useCallback } from 'react';
-import { useDispatch } from 'react-redux';
+import React from 'react';
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,
@@ -42,14 +40,7 @@ import FilterableTable, {
import CopyToClipboard from 'src/components/CopyToClipboard';
import { addDangerToast } from 'src/components/MessageToasts/actions';
import { prepareCopyToClipboardTabularData } from 'src/utils/common';
-import {
- CtasEnum,
- clearQueryResults,
- addQueryEditor,
- fetchQueryResults,
- reFetchQueryResults,
- reRunQuery,
-} from 'src/SqlLab/actions/sqlLab';
+import { CtasEnum } from 'src/SqlLab/actions/sqlLab';
import { URL_PARAMS } from 'src/constants';
import ExploreCtasResultsButton from '../ExploreCtasResultsButton';
import ExploreResultsButton from '../ExploreResultsButton';
@@ -63,7 +54,9 @@ enum LIMITING_FACTOR {
NOT_LIMITED = 'NOT_LIMITED',
}
-export interface ResultSetProps {
+interface ResultSetProps {
+ showControls?: boolean;
+ actions: Record<string, any>;
cache?: boolean;
csv?: boolean;
database?: Record<string, any>;
@@ -77,9 +70,17 @@ export 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: ${({ theme }) => theme.gridUnit * 25}px;
+ min-height: 100px;
[role='alert'] {
margin-top: ${({ theme }) => theme.gridUnit * 2}px;
}
@@ -99,8 +100,8 @@ const MonospaceDiv = styled.div`
`;
const ReturnedRows = styled.div`
- font-size: ${({ theme }) => theme.typography.sizes.s}px;
- line-height: ${({ theme }) => theme.gridUnit * 6}px;
+ font-size: 13px;
+ line-height: 24px;
`;
const ResultSetControls = styled.div`
@@ -120,84 +121,115 @@ const LimitMessage = styled.span`
margin-left: ${({ theme }) => theme.gridUnit * 2}px;
`;
-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);
-
- const dispatch = useDispatch();
-
- const reRunQueryIfSessionTimeoutErrorOnMount = useCallback(() => {
- if (
- query.errorMessage &&
- query.errorMessage.indexOf('session timed out') > 0
- ) {
- dispatch(reRunQuery(query));
- }
- }, []);
+export default class ResultSet extends React.PureComponent<
+ ResultSetProps,
+ ResultSetState
+> {
+ static defaultProps = {
+ cache: false,
+ csv: true,
+ database: {},
+ search: true,
+ showSql: false,
+ visualize: true,
+ };
- useEffect(() => {
- // only do this the first time the component is rendered/mounted
- reRunQueryIfSessionTimeoutErrorOnMount();
- }, [reRunQueryIfSessionTimeoutErrorOnMount]);
+ 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 fetchResults = (query: QueryResponse) => {
- dispatch(fetchQueryResults(query, displayLimit));
- };
+ async componentDidMount() {
+ // only do this the first time the component is rendered/mounted
+ this.reRunQueryIfSessionTimeoutErrorOnMount();
+ }
- const prevQuery = usePrevious(query);
- useEffect(() => {
- if (cache && query.cached && query?.results?.data?.length > 0) {
- setCachedData(query.results.data);
- dispatch(clearQueryResults(query));
+ 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 (
- query.resultsKey &&
- prevQuery?.resultsKey &&
- query.resultsKey !== prevQuery.resultsKey
+ nextProps.query.resultsKey &&
+ nextProps.query.resultsKey !== this.props.query.resultsKey
) {
- fetchResults(query);
+ this.fetchResults(nextProps.query);
}
- }, [query, cache]);
+ }
- const calculateAlertRefHeight = (alertElement: HTMLElement | null) => {
+ calculateAlertRefHeight = (alertElement: HTMLElement | null) => {
if (alertElement) {
- setAlertIsOpen(true);
+ this.setState({ alertIsOpen: true });
} else {
- setAlertIsOpen(false);
+ this.setState({ alertIsOpen: false });
}
};
- const popSelectStar = (tempSchema: string | null, tempTable: string) => {
+ clearQueryResults(query: QueryResponse) {
+ this.props.actions.clearQueryResults(query);
+ }
+
+ popSelectStar(tempSchema: string | null, tempTable: string) {
const qe = {
id: shortid.generate(),
name: tempTable,
autorun: false,
- dbId: query.dbId,
+ dbId: this.props.query.dbId,
sql: `SELECT * FROM ${tempSchema ? `${tempSchema}.` : ''}${tempTable}`,
};
- dispatch(addQueryEditor(qe));
- };
+ this.props.actions.addQueryEditor(qe);
+ }
- const changeSearch = (event: React.ChangeEvent<HTMLInputElement>) => {
- setSearchText(event.target.value);
- };
+ 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);
+ }
- const createExploreResultsOnClick = async () => {
- const { results } = query;
+ reFetchQueryResults(query: QueryResponse) {
+ this.props.actions.reFetchQueryResults(query);
+ }
+
+ reRunQueryIfSessionTimeoutErrorOnMount() {
+ const { query } = this.props;
+ if (
+ query.errorMessage &&
+ query.errorMessage.indexOf('session timed out') > 0
+ ) {
+ this.props.actions.reRunQuery(query);
+ }
+ }
+
+ createExploreResultsOnClick = async () => {
+ const { results } = this.props.query;
if (results?.query_id) {
const key = await postFormData(results.query_id, 'query', {
@@ -216,14 +248,16 @@ const ResultSet = ({
}
};
- const renderControls = () => {
- if (search || visualize || csv) {
- let { data } = query.results;
- if (cache && query.cached) {
- data = cachedData;
+ 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 { columns } = query.results;
+ const { columns } = this.props.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[],
@@ -238,7 +272,7 @@ const ResultSet = ({
<ResultSetControls>
<SaveDatasetModal
visible={showSaveDatasetModal}
- onHide={() => setShowSaveDatasetModal(false)}
+ onHide={() => this.setState({ showSaveDatasetModal: false })}
buttonTextOnSave={t('Save & Explore')}
buttonTextOnOverwrite={t('Overwrite & Explore')}
modalDescription={t(
@@ -247,13 +281,14 @@ const ResultSet = ({
datasource={datasource}
/>
<ResultSetButtons>
- {visualize && database?.allows_virtual_table_explore && (
- <ExploreResultsButton
- database={database}
- onClick={createExploreResultsOnClick}
- />
- )}
- {csv && (
+ {this.props.visualize &&
+ this.props.database?.allows_virtual_table_explore && (
+ <ExploreResultsButton
+ database={this.props.database}
+ onClick={this.createExploreResultsOnClick}
+ />
+ )}
+ {this.props.csv && (
<Button buttonSize="small" href={`/superset/csv/${query.id}`}>
<i className="fa fa-file-text-o" /> {t('Download to CSV')}
</Button>
@@ -270,11 +305,11 @@ const ResultSet = ({
hideTooltip
/>
</ResultSetButtons>
- {search && (
+ {this.props.search && (
<input
type="text"
- onChange={changeSearch}
- value={searchText}
+ onChange={this.changeSearch}
+ value={this.state.searchText}
className="form-control input-sm"
disabled={columns.length > MAX_COLUMNS_FOR_TABLE}
placeholder={
@@ -288,14 +323,14 @@ const ResultSet = ({
);
}
return <div />;
- };
+ }
- const renderRowsReturned = () => {
- const { results, rows, queryLimit, limitingFactor } = query;
+ renderRowsReturned() {
+ const { results, rows, queryLimit, limitingFactor } = this.props.query;
let limitMessage;
const limitReached = results?.displayLimitReached;
const limit = queryLimit || results.query.limit;
- const isAdmin = !!user?.roles?.Admin;
+ const isAdmin = !!this.props.user?.roles?.Admin;
const rowsCount = Math.min(rows || 0, results?.data?.length || 0);
const displayMaxRowsReachedMessage = {
@@ -313,10 +348,10 @@ const ResultSet = ({
),
};
const shouldUseDefaultDropdownAlert =
- limit === defaultQueryLimit &&
+ limit === this.props.defaultQueryLimit &&
limitingFactor === LIMITING_FACTOR.DROPDOWN;
- if (limitingFactor === LIMITING_FACTOR.QUERY && csv) {
+ if (limitingFactor === LIMITING_FACTOR.QUERY && this.props.csv) {
limitMessage = t(
'The number of rows displayed is limited to %(rows)d by the query',
{ rows },
@@ -351,11 +386,11 @@ const ResultSet = ({
</span>
)}
{!limitReached && shouldUseDefaultDropdownAlert && (
- <div ref={calculateAlertRefHeight}>
+ <div ref={this.calculateAlertRefHeight}>
<Alert
type="warning"
message={t('%(rows)d rows returned', { rows })}
- onClose={() => setAlertIsOpen(false)}
+ onClose={() => this.setState({ alertIsOpen: false })}
description={t(
'The number of rows displayed is limited to %s by the dropdown.',
rows,
@@ -364,10 +399,10 @@ const ResultSet = ({
</div>
)}
{limitReached && (
- <div ref={calculateAlertRefHeight}>
+ <div ref={this.calculateAlertRefHeight}>
<Alert
type="warning"
- onClose={() => setAlertIsOpen(false)}
+ onClose={() => this.setState({ alertIsOpen: false })}
message={t('%(rows)d rows returned', { rows: rowsCount })}
description={
isAdmin
@@ -379,191 +414,193 @@ const ResultSet = ({
)}
</ReturnedRows>
);
- };
-
- const limitReached = query?.results?.displayLimitReached;
- let sql;
- let exploreDBId = query.dbId;
- if (database?.explore_database_id) {
- exploreDBId = 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>
- );
- }
-
- if (showSql) {
- sql = <HighlightedSql sql={query.sql} />;
- }
-
- if (query.state === 'stopped') {
- return <Alert type="warning" message={t('Query was stopped')} />;
- }
+ 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>
+ );
+ }
- 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 (this.props.showSql) sql = <HighlightedSql sql={query.sql} />;
- if (query.state === 'success' && query.ctas) {
- const { tempSchema, tempTable } = query;
- let object = 'Table';
- if (query.ctas_method === CtasEnum.VIEW) {
- object = 'View';
+ if (query.state === 'stopped') {
+ return <Alert type="warning" message={t('Query was stopped')} />;
}
- return (
- <div>
- <Alert
- type="info"
- message={
- <>
- {t(object)} [
- <strong>
- {tempSchema ? `${tempSchema}.` : ''}
- {tempTable}
- </strong>
- ] {t('was created')}
- <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 (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 (data && data.length > 0) {
- const expandedColumns = results.expanded_columns
- ? results.expanded_columns.map(col => col.name)
- : [];
+ if (query.state === 'success' && query.ctas) {
+ const { tempSchema, tempTable } = query;
+ let object = 'Table';
+ if (query.ctas_method === CtasEnum.VIEW) {
+ object = 'View';
+ }
return (
- <>
- {renderControls()}
- {renderRowsReturned()}
- {sql}
- <FilterableTable
- data={data}
- orderedColumnKeys={results.columns.map(col => col.name)}
- height={rowsHeight}
- filterText={searchText}
- expandedColumns={expandedColumns}
+ <div>
+ <Alert
+ type="info"
+ message={
+ <>
+ {t(object)} [
+ <strong>
+ {tempSchema ? `${tempSchema}.` : ''}
+ {tempTable}
+ </strong>
+ ] {t('was created')}
+ <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 (data && data.length === 0) {
- return <Alert type="warning" message={t('The query returned no data')} />;
+ 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={() =>
- dispatch(
- reFetchQueryResults({
+ 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>
- );
+ })
+ }
+ >
+ {t('Fetch data preview')}
+ </Button>
+ );
+ }
+ if (query.resultsKey) {
+ return (
+ <Button
+ buttonSize="small"
+ buttonStyle="primary"
+ onClick={() => this.fetchResults(query)}
+ >
+ {t('Refetch results')}
+ </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 && query.extra && query.extra.progress
+ ? query.extra.progress
+ : null;
- let progressBar;
- if (query.progress > 0) {
- progressBar = (
- <ProgressBar percent={parseInt(query.progress.toFixed(0), 10)} striped />
+ 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>
);
}
-
- 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;
+}
diff --git a/superset-frontend/src/SqlLab/components/SouthPane/index.tsx b/superset-frontend/src/SqlLab/components/SouthPane/index.tsx
index 2be88f6fe2..ddcd972f98 100644
--- a/superset-frontend/src/SqlLab/components/SouthPane/index.tsx
+++ b/superset-frontend/src/SqlLab/components/SouthPane/index.tsx
@@ -164,8 +164,10 @@ export default function SouthPane({
if (Date.now() - latestQuery.startDttm <= LOCALSTORAGE_MAX_QUERY_AGE_MS) {
results = (
<ResultSet
+ showControls
search
query={latestQuery}
+ actions={actions}
user={user}
height={innerTabContentHeight + EXTRA_HEIGHT_RESULTS}
database={databases[latestQuery.dbId]}
@@ -197,6 +199,7 @@ export default function SouthPane({
query={query}
visualize={false}
csv={false}
+ actions={actions}
cache
user={user}
height={innerTabContentHeight}
diff --git a/superset-frontend/src/components/FilterableTable/index.tsx b/superset-frontend/src/components/FilterableTable/index.tsx
index 16ae37e671..8324e93b90 100644
--- a/superset-frontend/src/components/FilterableTable/index.tsx
+++ b/superset-frontend/src/components/FilterableTable/index.tsx
@@ -578,7 +578,7 @@ const FilterableTable = ({
// fix height of filterable table
return (
- <StyledFilterableTable data-test="grid-container">
+ <StyledFilterableTable>
<ScrollSync>
{({ onScroll, scrollLeft }) => (
<>
@@ -659,7 +659,6 @@ const FilterableTable = ({
return (
<StyledFilterableTable
className="filterable-table-container"
- data-test="table-container"
ref={container}
>
{fitted && (
diff --git a/superset-frontend/src/components/ProgressBar/index.tsx b/superset-frontend/src/components/ProgressBar/index.tsx
index ba69fc90c6..93b4315e52 100644
--- a/superset-frontend/src/components/ProgressBar/index.tsx
+++ b/superset-frontend/src/components/ProgressBar/index.tsx
@@ -27,7 +27,7 @@ export interface ProgressBarProps extends ProgressProps {
// eslint-disable-next-line @typescript-eslint/no-unused-vars
const ProgressBar = styled(({ striped, ...props }: ProgressBarProps) => (
- <AntdProgress data-test="progress-bar" {...props} />
+ <AntdProgress {...props} />
))`
line-height: 0;
position: static;