You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@superset.apache.org by kg...@apache.org on 2022/09/06 14:55:23 UTC
[superset] branch master updated: chore: refactor ResultSet to functional component (#21186)
This is an automated email from the ASF dual-hosted git repository.
kgabryje pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/superset.git
The following commit(s) were added to refs/heads/master by this push:
new f603295678 chore: refactor ResultSet to functional component (#21186)
f603295678 is described below
commit f6032956780380bdac1e3ae950687ae53eabf2e1
Author: EugeneTorap <ev...@gmail.com>
AuthorDate: Tue Sep 6 17:55:07 2022 +0300
chore: refactor ResultSet to functional component (#21186)
---
.../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, 501 insertions(+), 542 deletions(-)
diff --git a/superset-frontend/src/SqlLab/components/ExploreCtasResultsButton/index.tsx b/superset-frontend/src/SqlLab/components/ExploreCtasResultsButton/index.tsx
index fbcdc15bc5..ac9e8b2fb4 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 } from 'react-redux';
-import { t } from '@superset-ui/core';
+import { useSelector, useDispatch } from 'react-redux';
+import { t, JsonObject } from '@superset-ui/core';
+import {
+ createCtasDatasource,
+ addInfoToast,
+ addDangerToast,
+} from 'src/SqlLab/actions/sqlLab';
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,16 +37,15 @@ 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,
@@ -56,7 +55,7 @@ const ExploreCtasResultsButton = ({
};
const visualize = () => {
- createCtasDatasource(buildVizOptions)
+ dispatch(createCtasDatasource(buildVizOptions))
.then((data: { table_id: number }) => {
const formData = {
datasource: `${data.table_id}__table`,
@@ -67,12 +66,14 @@ const ExploreCtasResultsButton = ({
all_columns: [],
row_limit: 1000,
};
- addInfoToast(t('Creating a data source and creating a new tab'));
+ dispatch(
+ addInfoToast(t('Creating a data source and creating a new tab')),
+ );
// open new window for data visualization
exploreChart(formData);
})
.catch(() => {
- addDangerToast(errorMessage || t('An error occurred'));
+ dispatch(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 24d5e8686f..4ab7777736 100644
--- a/superset-frontend/src/SqlLab/components/ExploreResultsButton/index.tsx
+++ b/superset-frontend/src/SqlLab/components/ExploreResultsButton/index.tsx
@@ -39,6 +39,7 @@ 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 54edb7f97e..d7ef5ed51c 100644
--- a/superset-frontend/src/SqlLab/components/QueryTable/index.tsx
+++ b/superset-frontend/src/SqlLab/components/QueryTable/index.tsx
@@ -245,7 +245,6 @@ 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
deleted file mode 100644
index c04e236133..0000000000
--- a/superset-frontend/src/SqlLab/components/ResultSet/ResultSet.test.jsx
+++ /dev/null
@@ -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();
-});
diff --git a/superset-frontend/src/SqlLab/components/ResultSet/ResultSet.test.tsx b/superset-frontend/src/SqlLab/components/ResultSet/ResultSet.test.tsx
new file mode 100644
index 0000000000..2818ed279c
--- /dev/null
+++ b/superset-frontend/src/SqlLab/components/ResultSet/ResultSet.test.tsx
@@ -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();
+ });
+});
diff --git a/superset-frontend/src/SqlLab/components/ResultSet/index.tsx b/superset-frontend/src/SqlLab/components/ResultSet/index.tsx
index 27913d7fc4..78387f6dc4 100644
--- a/superset-frontend/src/SqlLab/components/ResultSet/index.tsx
+++ b/superset-frontend/src/SqlLab/components/ResultSet/index.tsx
@@ -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,
- };
-
- 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 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));
+ }
+ }, []);
- async componentDidMount() {
+ useEffect(() => {
// only do this the first time the component is rendered/mounted
- this.reRunQueryIfSessionTimeoutErrorOnMount();
- }
+ reRunQueryIfSessionTimeoutErrorOnMount();
+ }, [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),
- );
+ 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 (
- nextProps.query.resultsKey &&
- nextProps.query.resultsKey !== this.props.query.resultsKey
+ query.resultsKey &&
+ prevQuery?.resultsKey &&
+ query.resultsKey !== prevQuery.resultsKey
) {
- this.fetchResults(nextProps.query);
+ fetchResults(query);
}
- }
+ }, [query, cache]);
- calculateAlertRefHeight = (alertElement: HTMLElement | null) => {
+ const calculateAlertRefHeight = (alertElement: HTMLElement | null) => {
if (alertElement) {
- this.setState({ alertIsOpen: true });
+ setAlertIsOpen(true);
} else {
- this.setState({ alertIsOpen: false });
+ setAlertIsOpen(false);
}
};
- clearQueryResults(query: QueryResponse) {
- this.props.actions.clearQueryResults(query);
- }
-
- popSelectStar(tempSchema: string | null, tempTable: string) {
+ const popSelectStar = (tempSchema: string | null, tempTable: string) => {
const qe = {
id: shortid.generate(),
name: tempTable,
autorun: false,
- dbId: this.props.query.dbId,
+ dbId: 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);
- }
+ dispatch(addQueryEditor(qe));
+ };
- reRunQueryIfSessionTimeoutErrorOnMount() {
- const { query } = this.props;
- if (
- query.errorMessage &&
- query.errorMessage.indexOf('session timed out') > 0
- ) {
- this.props.actions.reRunQuery(query);
- }
- }
+ const changeSearch = (event: React.ChangeEvent<HTMLInputElement>) => {
+ setSearchText(event.target.value);
+ };
- createExploreResultsOnClick = async () => {
- const { results } = this.props.query;
+ 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 === 'stopped') {
- return <Alert type="warning" message={t('Query was stopped')} />;
+ if (query.state === 'success' && query.ctas) {
+ const { tempSchema, tempTable } = query;
+ let object = 'Table';
+ if (query.ctas_method === CtasEnum.VIEW) {
+ object = 'View';
}
- 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>
- );
+ 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 === 'success' && query.ctas) {
- const { tempSchema, tempTable } = query;
- let object = 'Table';
- if (query.ctas_method === CtasEnum.VIEW) {
- object = 'View';
- }
+ if (data && data.length > 0) {
+ const expandedColumns = results.expanded_columns
+ ? results.expanded_columns.map(col => col.name)
+ : [];
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={() => 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>
- </>
- }
+ <>
+ {renderControls()}
+ {renderRowsReturned()}
+ {sql}
+ <FilterableTable
+ data={data}
+ orderedColumnKeys={results.columns.map(col => col.name)}
+ height={rowsHeight}
+ filterText={searchText}
+ expandedColumns={expandedColumns}
/>
- </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 (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({
+ }
+
+ 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={() => this.fetchResults(query)}
- >
- {t('Refetch results')}
- </Button>
- );
- }
+ }),
+ )
+ }
+ >
+ {t('Fetch data preview')}
+ </Button>
+ );
}
- let progressBar;
- if (query.progress > 0) {
- progressBar = (
- <ProgressBar
- percent={parseInt(query.progress.toFixed(0), 10)}
- striped
- />
+ if (query.resultsKey) {
+ return (
+ <Button
+ buttonSize="small"
+ buttonStyle="primary"
+ onClick={() => fetchResults(query)}
+ >
+ {t('Refetch results')}
+ </Button>
);
}
- const progressMsg =
- query && query.extra && query.extra.progress
- ? 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>
+ 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;
diff --git a/superset-frontend/src/SqlLab/components/SouthPane/index.tsx b/superset-frontend/src/SqlLab/components/SouthPane/index.tsx
index ddcd972f98..2be88f6fe2 100644
--- a/superset-frontend/src/SqlLab/components/SouthPane/index.tsx
+++ b/superset-frontend/src/SqlLab/components/SouthPane/index.tsx
@@ -164,10 +164,8 @@ 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]}
@@ -199,7 +197,6 @@ 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 8324e93b90..16ae37e671 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>
+ <StyledFilterableTable data-test="grid-container">
<ScrollSync>
{({ onScroll, scrollLeft }) => (
<>
@@ -659,6 +659,7 @@ 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 93b4315e52..ba69fc90c6 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 {...props} />
+ <AntdProgress data-test="progress-bar" {...props} />
))`
line-height: 0;
position: static;