From f6032956780380bdac1e3ae950687ae53eabf2e1 Mon Sep 17 00:00:00 2001 From: EugeneTorap Date: Tue, 6 Sep 2022 17:55:07 +0300 Subject: [PATCH] chore: refactor ResultSet to functional component (#21186) --- .../ExploreCtasResultsButton/index.tsx | 25 +- .../components/ExploreResultsButton/index.tsx | 1 + .../SqlLab/components/QueryTable/index.tsx | 1 - .../components/ResultSet/ResultSet.test.jsx | 219 ------- .../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(-) delete mode 100644 superset-frontend/src/SqlLab/components/ResultSet/ResultSet.test.jsx create mode 100644 superset-frontend/src/SqlLab/components/ResultSet/ResultSet.test.tsx diff --git a/superset-frontend/src/SqlLab/components/ExploreCtasResultsButton/index.tsx b/superset-frontend/src/SqlLab/components/ExploreCtasResultsButton/index.tsx index fbcdc15bc5a37..ac9e8b2fb453c 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>(); 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 24d5e8686f71b..4ab77777367fc 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" > { - expect(React.isValidElement()).toBe(true); -}); - -test('renders a Table', () => { - const wrapper = shallow(); - 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(); - expect(reRunQuerySpy.callCount).toBe(1); - }); - - it('should not call reRunQuery if no error', () => { - shallow(); - expect(reRunQuerySpy.callCount).toBe(0); - }); -}); - -describe('UNSAFE_componentWillReceiveProps', () => { - const wrapper = shallow(); - 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(); - 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( - - - , - ); - 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(); - 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(); - expect(wrapper.find(Alert)).toExist(); -}); - -test('should render running/pending/fetching query', () => { - const wrapper = shallow(); - expect(wrapper.find(ProgressBar)).toExist(); -}); - -test('should render fetching w/ 100 progress query', () => { - const wrapper = shallow(); - expect(wrapper.find(Loading)).toExist(); -}); - -test('should render a failed query with an error message', () => { - const wrapper = shallow(); - expect(wrapper.find(ErrorMessageWithStackTrace)).toExist(); -}); - -test('should render a failed query with an errors object', () => { - const wrapper = shallow(); - expect(wrapper.find(ErrorMessageWithStackTrace)).toExist(); -}); - -test('renders if there is no limit in query.results but has queryLimit', () => { - render(, { 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(, { 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 0000000000000..2818ed279c7d8 --- /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(, { + 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(); + 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 27913d7fc4a9d..78387f6dc44d4 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; +export interface ResultSetProps { cache?: boolean; csv?: boolean; database?: Record; @@ -70,17 +77,9 @@ interface ResultSetProps { defaultQueryLimit: number; } -interface ResultSetState { - searchText: string; - showExploreResultsButton: boolean; - data: Record[]; - 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[]>([]); + 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) { - 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) => { + 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< 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} /> - {this.props.visualize && - this.props.database?.allows_virtual_table_explore && ( - - )} - {this.props.csv && ( + {visualize && database?.allows_virtual_table_explore && ( + + )} + {csv && ( @@ -305,11 +270,11 @@ export default class ResultSet extends React.PureComponent< hideTooltip /> - {this.props.search && ( + {search && ( MAX_COLUMNS_FOR_TABLE} placeholder={ @@ -323,14 +288,14 @@ export default class ResultSet extends React.PureComponent< ); } return
; - } + }; - 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< )} {!limitReached && shouldUseDefaultDropdownAlert && ( -
+
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<
)} {limitReached && ( -
+
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< )} ); + }; + + 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 = ( - - ); - } + let trackingUrl; + if ( + query.trackingUrl && + query.state !== 'success' && + query.state !== 'fetching' + ) { + trackingUrl = ( + + ); + } - if (this.props.showSql) sql = ; + if (showSql) { + sql = ; + } + + if (query.state === 'stopped') { + return ; + } + + if (query.state === 'failed') { + return ( + + {query.errorMessage}} + copyText={query.errorMessage || undefined} + link={query.link} + source="sqllab" + /> + {trackingUrl} + + ); + } - if (query.state === 'stopped') { - return ; + 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 ( - - {query.errorMessage}} - copyText={query.errorMessage || undefined} - link={query.link} - source="sqllab" - /> - {trackingUrl} - - ); + return ( +
+ + {t(object)} [ + + {tempSchema ? `${tempSchema}.` : ''} + {tempTable} + + ] {t('was created')}   + + + + + + } + /> +
+ ); + } + + 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 ( -
- - {t(object)} [ - - {tempSchema ? `${tempSchema}.` : ''} - {tempTable} - - ] {t('was created')}   - - - - - - } + <> + {renderControls()} + {renderRowsReturned()} + {sql} + col.name)} + height={rowsHeight} + filterText={searchText} + expandedColumns={expandedColumns} /> -
+ ); } - 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} - col.name)} - height={height} - filterText={this.state.searchText} - expandedColumns={expandedColumns} - /> - - ); - } - if (data && data.length === 0) { - return ( - - ); - } + if (data && data.length === 0) { + return ; } - if (query.cached || (query.state === 'success' && !query.results)) { - if (query.isDataPreview) { - return ( - - ); - } - if (query.resultsKey) { - return ( - - ); - } + }), + ) + } + > + {t('Fetch data preview')} + + ); } - let progressBar; - if (query.progress > 0) { - progressBar = ( - + if (query.resultsKey) { + return ( + ); } - const progressMsg = - query && query.extra && query.extra.progress - ? query.extra.progress - : null; + } - return ( - -
{!progressBar && }
- {/* show loading bar whenever progress bar is completed but needs time to render */} -
{query.progress === 100 && }
- -
- {progressMsg && } -
-
{query.progress !== 100 && progressBar}
- {trackingUrl &&
{trackingUrl}
} -
+ let progressBar; + if (query.progress > 0) { + progressBar = ( + ); } -} + + const progressMsg = query?.extra?.progress ?? null; + + return ( + +
{!progressBar && }
+ {/* show loading bar whenever progress bar is completed but needs time to render */} +
{query.progress === 100 && }
+ +
{progressMsg && }
+
{query.progress !== 100 && progressBar}
+ {trackingUrl &&
{trackingUrl}
} +
+ ); +}; + +export default ResultSet; diff --git a/superset-frontend/src/SqlLab/components/SouthPane/index.tsx b/superset-frontend/src/SqlLab/components/SouthPane/index.tsx index ddcd972f9828b..2be88f6fe2189 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 = ( + {({ onScroll, scrollLeft }) => ( <> @@ -659,6 +659,7 @@ const FilterableTable = ({ return ( {fitted && ( diff --git a/superset-frontend/src/components/ProgressBar/index.tsx b/superset-frontend/src/components/ProgressBar/index.tsx index 93b4315e523d7..ba69fc90c6cab 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) => ( - + ))` line-height: 0; position: static;