diff --git a/superset-frontend/src/dashboard/components/SliceHeaderControls/index.tsx b/superset-frontend/src/dashboard/components/SliceHeaderControls/index.tsx index dcec62d88c0e4..6b39e18449354 100644 --- a/superset-frontend/src/dashboard/components/SliceHeaderControls/index.tsx +++ b/superset-frontend/src/dashboard/components/SliceHeaderControls/index.tsx @@ -36,7 +36,7 @@ import Icons from 'src/components/Icons'; import ModalTrigger from 'src/components/ModalTrigger'; import Button from 'src/components/Button'; import ViewQueryModal from 'src/explore/components/controls/ViewQueryModal'; -import { ResultsPane } from 'src/explore/components/DataTablesPane'; +import { ResultsPaneOnDashboard } from 'src/explore/components/DataTablesPane'; const MENU_KEYS = { CROSS_FILTER_SCOPING: 'cross_filter_scoping', @@ -340,11 +340,12 @@ class SliceHeaderControls extends React.PureComponent< } modalTitle={t('Chart Data: %s', slice.slice_name)} modalBody={ - } modalFooter={ diff --git a/superset-frontend/src/explore/components/DataTableControl/index.tsx b/superset-frontend/src/explore/components/DataTableControl/index.tsx index fb8af865a3914..fdc74d7bb4cde 100644 --- a/superset-frontend/src/explore/components/DataTableControl/index.tsx +++ b/superset-frontend/src/explore/components/DataTableControl/index.tsx @@ -163,6 +163,7 @@ const DataTableTemporalHeaderCell = ({ columnName, onTimeColumnChange, datasourceId, + isOriginalTimeColumn, }: { columnName: string; onTimeColumnChange: ( @@ -170,15 +171,12 @@ const DataTableTemporalHeaderCell = ({ columnType: FormatPickerValue, ) => void; datasourceId?: string; + isOriginalTimeColumn: boolean; }) => { const theme = useTheme(); - const [isOriginalTimeColumn, setIsOriginalTimeColumn] = useState( - getTimeColumns(datasourceId).includes(columnName), - ); const onChange = (e: any) => { onTimeColumnChange(columnName, e.target.value); - setIsOriginalTimeColumn(getTimeColumns(datasourceId).includes(columnName)); }; const overlayContent = useMemo( @@ -313,6 +311,8 @@ export const useTableColumns = ( colType === GenericDataType.TEMPORAL ? originalFormattedTimeColumns.indexOf(key) : -1; + const isOriginalTimeColumn = + originalFormattedTimeColumns.includes(key); return { id: key, accessor: row => row[key], @@ -324,6 +324,7 @@ export const useTableColumns = ( columnName={key} datasourceId={datasourceId} onTimeColumnChange={onTimeColumnChange} + isOriginalTimeColumn={isOriginalTimeColumn} /> ) : ( key diff --git a/superset-frontend/src/explore/components/DataTablesPane/DataTablesPane.tsx b/superset-frontend/src/explore/components/DataTablesPane/DataTablesPane.tsx index 99b7059c632db..bfba9cf980011 100644 --- a/superset-frontend/src/explore/components/DataTablesPane/DataTablesPane.tsx +++ b/superset-frontend/src/explore/components/DataTablesPane/DataTablesPane.tsx @@ -31,13 +31,12 @@ import { setItem, LocalStorageKeys, } from 'src/utils/localStorageHelpers'; -import { ResultsPane, SamplesPane, TableControlsWrapper } from './components'; -import { DataTablesPaneProps } from './types'; - -enum ResultTypes { - Results = 'results', - Samples = 'samples', -} +import { + SamplesPane, + TableControlsWrapper, + useResultsPane, +} from './components'; +import { DataTablesPaneProps, ResultTypes } from './types'; const SouthPane = styled.div` ${({ theme }) => ` @@ -114,7 +113,7 @@ export const DataTablesPane = ({ if ( panelOpen && - activeTabKey === ResultTypes.Results && + activeTabKey.startsWith(ResultTypes.Results) && chartStatus === 'rendered' ) { setIsRequest({ @@ -187,6 +186,35 @@ export const DataTablesPane = ({ ); }, [handleCollapseChange, panelOpen, theme.colors.grayscale.base]); + const queryResultsPanes = useResultsPane({ + errorMessage, + queryFormData, + queryForce, + ownState, + isRequest: isRequest.results, + actions, + isVisible: ResultTypes.Results === activeTabKey, + }).map((pane, idx) => { + if (idx === 0) { + return ( + + {pane} + + ); + } + if (idx > 0) { + return ( + + {pane} + + ); + } + return null; + }); + return ( - - - + {queryResultsPanes} diff --git a/superset-frontend/src/explore/components/DataTablesPane/components/ResultsPaneOnDashboard.tsx b/superset-frontend/src/explore/components/DataTablesPane/components/ResultsPaneOnDashboard.tsx new file mode 100644 index 0000000000000..3f27929f5cd8b --- /dev/null +++ b/superset-frontend/src/explore/components/DataTablesPane/components/ResultsPaneOnDashboard.tsx @@ -0,0 +1,69 @@ +/** + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +import React from 'react'; +import { t } from '@superset-ui/core'; +import Tabs from 'src/components/Tabs'; +import { ResultTypes, ResultsPaneProps } from '../types'; +import { useResultsPane } from './useResultsPane'; + +export const ResultsPaneOnDashboard = ({ + isRequest, + queryFormData, + queryForce, + ownState, + errorMessage, + actions, + isVisible, + dataSize = 50, +}: ResultsPaneProps) => { + const resultsPanes = useResultsPane({ + errorMessage, + queryFormData, + queryForce, + ownState, + isRequest, + actions, + dataSize, + isVisible, + }); + if (resultsPanes.length === 1) { + return resultsPanes[0]; + } + + const panes = resultsPanes.map((pane, idx) => { + if (idx === 0) { + return ( + + {pane} + + ); + } + + return ( + + {pane} + + ); + }); + + return {panes} ; +}; diff --git a/superset-frontend/src/explore/components/DataTablesPane/components/SamplesPane.tsx b/superset-frontend/src/explore/components/DataTablesPane/components/SamplesPane.tsx index 1997acf596ede..8b1137334b9de 100644 --- a/superset-frontend/src/explore/components/DataTablesPane/components/SamplesPane.tsx +++ b/superset-frontend/src/explore/components/DataTablesPane/components/SamplesPane.tsx @@ -41,6 +41,7 @@ export const SamplesPane = ({ queryForce, actions, dataSize = 50, + isVisible, }: SamplesPaneProps) => { const [filterText, setFilterText] = useState(''); const [data, setData] = useState[][]>([]); @@ -90,7 +91,7 @@ export const SamplesPane = ({ coltypes, data, datasourceId, - isRequest, + isVisible, ); const filteredData = useFilteredTableData(filterText, data); diff --git a/superset-frontend/src/explore/components/DataTablesPane/components/SingleQueryResultPane.tsx b/superset-frontend/src/explore/components/DataTablesPane/components/SingleQueryResultPane.tsx new file mode 100644 index 0000000000000..27d312cc3ccda --- /dev/null +++ b/superset-frontend/src/explore/components/DataTablesPane/components/SingleQueryResultPane.tsx @@ -0,0 +1,73 @@ +/** + * 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, { useState } from 'react'; +import { t } from '@superset-ui/core'; +import TableView, { EmptyWrapperType } from 'src/components/TableView'; +import { + useFilteredTableData, + useTableColumns, +} from 'src/explore/components/DataTableControl'; +import { TableControls } from './DataTableControls'; +import { SingleQueryResultPaneProp } from '../types'; + +export const SingleQueryResultPane = ({ + data, + colnames, + coltypes, + datasourceId, + dataSize = 50, + isVisible, +}: SingleQueryResultPaneProp) => { + const [filterText, setFilterText] = useState(''); + + // this is to preserve the order of the columns, even if there are integer values, + // while also only grabbing the first column's keys + const columns = useTableColumns( + colnames, + coltypes, + data, + datasourceId, + isVisible, + ); + const filteredData = useFilteredTableData(filterText, data); + + return ( + <> + setFilterText(input)} + isLoading={false} + /> + + + ); +}; diff --git a/superset-frontend/src/explore/components/DataTablesPane/components/index.ts b/superset-frontend/src/explore/components/DataTablesPane/components/index.ts index 41623cb572083..e5762494c5576 100644 --- a/superset-frontend/src/explore/components/DataTablesPane/components/index.ts +++ b/superset-frontend/src/explore/components/DataTablesPane/components/index.ts @@ -16,6 +16,7 @@ * specific language governing permissions and limitations * under the License. */ -export { ResultsPane } from './ResultsPane'; +export { ResultsPaneOnDashboard } from './ResultsPaneOnDashboard'; export { SamplesPane } from './SamplesPane'; export { TableControls, TableControlsWrapper } from './DataTableControls'; +export { useResultsPane } from './useResultsPane'; diff --git a/superset-frontend/src/explore/components/DataTablesPane/components/ResultsPane.tsx b/superset-frontend/src/explore/components/DataTablesPane/components/useResultsPane.tsx similarity index 50% rename from superset-frontend/src/explore/components/DataTablesPane/components/ResultsPane.tsx rename to superset-frontend/src/explore/components/DataTablesPane/components/useResultsPane.tsx index d69a244430550..20e53df849cd1 100644 --- a/superset-frontend/src/explore/components/DataTablesPane/components/ResultsPane.tsx +++ b/superset-frontend/src/explore/components/DataTablesPane/components/useResultsPane.tsx @@ -17,18 +17,15 @@ * under the License. */ import React, { useState, useEffect } from 'react'; -import { ensureIsArray, GenericDataType, styled, t } from '@superset-ui/core'; +import { ensureIsArray, styled, t } from '@superset-ui/core'; import Loading from 'src/components/Loading'; import { EmptyStateMedium } from 'src/components/EmptyState'; -import TableView, { EmptyWrapperType } from 'src/components/TableView'; -import { - useFilteredTableData, - useTableColumns, -} from 'src/explore/components/DataTableControl'; import { getChartDataRequest } from 'src/components/Chart/chartAction'; import { getClientErrorObject } from 'src/utils/getClientErrorObject'; +import { ResultsPaneProps, QueryResultInterface } from '../types'; +import { getQueryCount } from '../utils'; +import { SingleQueryResultPane } from './SingleQueryResultPane'; import { TableControls } from './DataTableControls'; -import { ResultsPaneProps } from '../types'; const Error = styled.pre` margin-top: ${({ theme }) => `${theme.gridUnit * 4}px`}; @@ -36,21 +33,22 @@ const Error = styled.pre` const cache = new WeakSet(); -export const ResultsPane = ({ +export const useResultsPane = ({ isRequest, queryFormData, queryForce, ownState, errorMessage, actions, + isVisible, dataSize = 50, -}: ResultsPaneProps) => { - const [filterText, setFilterText] = useState(''); - const [data, setData] = useState[][]>([]); - const [colnames, setColnames] = useState([]); - const [coltypes, setColtypes] = useState([]); +}: ResultsPaneProps): React.ReactElement[] => { + const [resultResp, setResultResp] = useState([]); const [isLoading, setIsLoading] = useState(true); const [responseError, setResponseError] = useState(''); + const queryCount = getQueryCount( + queryFormData?.viz_type || queryFormData?.vizType, + ); useEffect(() => { // it's an invalid formData when gets a errorMessage @@ -65,28 +63,7 @@ export const ResultsPane = ({ ownState, }) .then(({ json }) => { - const { colnames, coltypes } = json.result[0]; - // Only displaying the first query is currently supported - if (json.result.length > 1) { - // todo: move these code to the backend, shouldn't loop by row in FE - const data: any[] = []; - json.result.forEach((item: { data: any[] }) => { - item.data.forEach((row, i) => { - if (data[i] !== undefined) { - data[i] = { ...data[i], ...row }; - } else { - data[i] = row; - } - }); - }); - setData(data); - setColnames(colnames); - setColtypes(coltypes); - } else { - setData(ensureIsArray(json.result[0].data)); - setColnames(colnames); - setColtypes(coltypes); - } + setResultResp(ensureIsArray(json.result)); setResponseError(''); cache.add(queryFormData); if (queryForce && actions) { @@ -110,68 +87,50 @@ export const ResultsPane = ({ } }, [errorMessage]); - // this is to preserve the order of the columns, even if there are integer values, - // while also only grabbing the first column's keys - const columns = useTableColumns( - colnames, - coltypes, - data, - queryFormData.datasource, - isRequest, - ); - const filteredData = useFilteredTableData(filterText, data); - if (isLoading) { - return ; + return Array(queryCount).fill(); } if (errorMessage) { const title = t('Run a query to display results'); - return ; + return Array(queryCount).fill( + , + ); } if (responseError) { - return ( + const err = ( <> setFilterText(input)} - isLoading={isLoading} + data={[]} + columnNames={[]} + columnTypes={[]} + datasourceId={queryFormData.datasource} + onInputChange={() => {}} + isLoading={false} /> {responseError} ); + return Array(queryCount).fill(err); } - if (data.length === 0) { + if (resultResp.length === 0) { const title = t('No results were returned for this query'); - return ; + return Array(queryCount).fill( + , + ); } - return ( - <> - setFilterText(input)} - isLoading={isLoading} - /> - - - ); + return resultResp.map((result, idx) => ( + + )); }; diff --git a/superset-frontend/src/explore/components/DataTablesPane/DataTablesPane.test.tsx b/superset-frontend/src/explore/components/DataTablesPane/test/DataTablesPane.test.tsx similarity index 64% rename from superset-frontend/src/explore/components/DataTablesPane/DataTablesPane.test.tsx rename to superset-frontend/src/explore/components/DataTablesPane/test/DataTablesPane.test.tsx index 57d599ee82b9a..c5d9d0c7bb2d3 100644 --- a/superset-frontend/src/explore/components/DataTablesPane/DataTablesPane.test.tsx +++ b/superset-frontend/src/explore/components/DataTablesPane/test/DataTablesPane.test.tsx @@ -16,7 +16,6 @@ * specific language governing permissions and limitations * under the License. */ - import React from 'react'; import userEvent from '@testing-library/user-event'; import fetchMock from 'fetch-mock'; @@ -26,56 +25,8 @@ import { screen, waitForElementToBeRemoved, } from 'spec/helpers/testing-library'; -import { DatasourceType } from '@superset-ui/core'; -import { exploreActions } from 'src/explore/actions/exploreActions'; -import { ChartStatus } from 'src/explore/types'; -import { DataTablesPane } from '.'; - -const createProps = () => ({ - queryFormData: { - viz_type: 'heatmap', - datasource: '34__table', - slice_id: 456, - url_params: {}, - time_range: 'Last week', - all_columns_x: 'source', - all_columns_y: 'target', - metric: 'sum__value', - adhoc_filters: [], - row_limit: 10000, - linear_color_scheme: 'blue_white_yellow', - xscale_interval: null, - yscale_interval: null, - canvas_image_rendering: 'pixelated', - normalize_across: 'heatmap', - left_margin: 'auto', - bottom_margin: 'auto', - y_axis_bounds: [null, null], - y_axis_format: 'SMART_NUMBER', - show_perc: true, - sort_x_axis: 'alpha_asc', - sort_y_axis: 'alpha_asc', - extra_form_data: {}, - }, - queryForce: false, - chartStatus: 'rendered' as ChartStatus, - onCollapseChange: jest.fn(), - queriesResponse: [ - { - colnames: [], - }, - ], - datasource: { - id: 0, - name: '', - type: DatasourceType.Table, - columns: [], - metrics: [], - columnFormats: {}, - verboseMap: {}, - }, - actions: exploreActions, -}); +import { DataTablesPane } from '..'; +import { createDataTablesPaneProps } from './fixture'; describe('DataTablesPane', () => { // Collapsed/expanded state depends on local storage @@ -89,7 +40,7 @@ describe('DataTablesPane', () => { }); test('Rendering DataTablesPane correctly', () => { - const props = createProps(); + const props = createDataTablesPaneProps(0); render(, { useRedux: true }); expect(screen.getByText('Results')).toBeVisible(); expect(screen.getByText('Samples')).toBeVisible(); @@ -97,7 +48,7 @@ describe('DataTablesPane', () => { }); test('Collapse/Expand buttons', async () => { - const props = createProps(); + const props = createDataTablesPaneProps(0); render(, { useRedux: true, }); @@ -112,7 +63,7 @@ describe('DataTablesPane', () => { }); test('Should show tabs: View results', async () => { - const props = createProps(); + const props = createDataTablesPaneProps(0); render(, { useRedux: true, }); @@ -121,9 +72,8 @@ describe('DataTablesPane', () => { expect(await screen.findByLabelText('Collapse data panel')).toBeVisible(); localStorage.clear(); }); - test('Should show tabs: View samples', async () => { - const props = createProps(); + const props = createDataTablesPaneProps(0); render(, { useRedux: true, }); @@ -146,31 +96,10 @@ describe('DataTablesPane', () => { }, ); const copyToClipboardSpy = jest.spyOn(copyUtils, 'default'); - const props = createProps(); - render( - , - { - useRedux: true, - initialState: { - explore: { - originalFormattedTimeColumns: { - '34__table': ['__timestamp'], - }, - }, - }, - }, - ); + const props = createDataTablesPaneProps(456); + render(, { + useRedux: true, + }); userEvent.click(screen.getByText('Results')); expect(await screen.findByText('1 row')).toBeVisible(); @@ -184,7 +113,7 @@ describe('DataTablesPane', () => { test('Search table', async () => { fetchMock.post( - 'glob:*/api/v1/chart/data?form_data=%7B%22slice_id%22%3A456%7D', + 'glob:*/api/v1/chart/data?form_data=%7B%22slice_id%22%3A789%7D', { result: [ { @@ -198,31 +127,10 @@ describe('DataTablesPane', () => { ], }, ); - const props = createProps(); - render( - , - { - useRedux: true, - initialState: { - explore: { - originalFormattedTimeColumns: { - '34__table': ['__timestamp'], - }, - }, - }, - }, - ); + const props = createDataTablesPaneProps(789); + render(, { + useRedux: true, + }); userEvent.click(screen.getByText('Results')); expect(await screen.findByText('2 rows')).toBeVisible(); expect(screen.getByText('Action')).toBeVisible(); diff --git a/superset-frontend/src/explore/components/DataTablesPane/test/ResultsPaneOnDashboard.test.tsx b/superset-frontend/src/explore/components/DataTablesPane/test/ResultsPaneOnDashboard.test.tsx new file mode 100644 index 0000000000000..19980ff711479 --- /dev/null +++ b/superset-frontend/src/explore/components/DataTablesPane/test/ResultsPaneOnDashboard.test.tsx @@ -0,0 +1,160 @@ +/** + * 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 fetchMock from 'fetch-mock'; +import userEvent from '@testing-library/user-event'; +import { + render, + waitForElementToBeRemoved, +} from 'spec/helpers/testing-library'; +import { exploreActions } from 'src/explore/actions/exploreActions'; +import { promiseTimeout } from '@superset-ui/core'; +import { ResultsPaneOnDashboard } from '../components'; +import { createResultsPaneOnDashboardProps } from './fixture'; + +describe('ResultsPaneOnDashboard', () => { + // render and render errorMessage + fetchMock.post( + 'end:/api/v1/chart/data?form_data=%7B%22slice_id%22%3A121%7D', + { + result: [], + }, + ); + + // force query, render and search + fetchMock.post( + 'end:/api/v1/chart/data?form_data=%7B%22slice_id%22%3A144%7D&force=true', + { + result: [ + { + data: [ + { __timestamp: 1230768000000, genre: 'Action' }, + { __timestamp: 1230768000010, genre: 'Horror' }, + ], + colnames: ['__timestamp', 'genre'], + coltypes: [2, 1], + }, + ], + }, + ); + + // error response + fetchMock.post( + 'end:/api/v1/chart/data?form_data=%7B%22slice_id%22%3A169%7D', + 400, + ); + + // multiple results pane + fetchMock.post( + 'end:/api/v1/chart/data?form_data=%7B%22slice_id%22%3A196%7D', + { + result: [ + { + data: [ + { __timestamp: 1230768000000 }, + { __timestamp: 1230768000010 }, + ], + colnames: ['__timestamp'], + coltypes: [2], + }, + { + data: [{ genre: 'Action' }, { genre: 'Horror' }], + colnames: ['genre'], + coltypes: [1], + }, + ], + }, + ); + + const setForceQuery = jest.spyOn(exploreActions, 'setForceQuery'); + + afterAll(() => { + fetchMock.reset(); + jest.resetAllMocks(); + }); + + test('render', async () => { + const props = createResultsPaneOnDashboardProps({ sliceId: 121 }); + const { findByText } = render(, { + useRedux: true, + }); + expect( + await findByText('No results were returned for this query'), + ).toBeVisible(); + }); + + test('render errorMessage', async () => { + const props = createResultsPaneOnDashboardProps({ + sliceId: 121, + errorMessage:

error

, + }); + const { findByText } = render(, { + useRedux: true, + }); + expect(await findByText('Run a query to display results')).toBeVisible(); + }); + + test('error response', async () => { + const props = createResultsPaneOnDashboardProps({ + sliceId: 169, + }); + const { findByText } = render(, { + useRedux: true, + }); + expect(await findByText('0 rows')).toBeVisible(); + expect(await findByText('Bad Request')).toBeVisible(); + }); + + test('force query, render and search', async () => { + const props = createResultsPaneOnDashboardProps({ + sliceId: 144, + queryForce: true, + }); + const { queryByText, getByPlaceholderText } = render( + , + { + useRedux: true, + }, + ); + + await promiseTimeout(() => { + expect(setForceQuery).toHaveBeenCalledTimes(1); + }, 10); + expect(queryByText('2 rows')).toBeVisible(); + expect(queryByText('Action')).toBeVisible(); + expect(queryByText('Horror')).toBeVisible(); + + userEvent.type(getByPlaceholderText('Search'), 'hor'); + await waitForElementToBeRemoved(() => queryByText('Action')); + expect(queryByText('Horror')).toBeVisible(); + expect(queryByText('Action')).not.toBeInTheDocument(); + }); + + test('multiple results pane', async () => { + const props = createResultsPaneOnDashboardProps({ + sliceId: 196, + vizType: 'mixed_timeseries', + }); + const { findByText } = render(, { + useRedux: true, + }); + expect(await findByText('Results')).toBeVisible(); + expect(await findByText('Results 2')).toBeVisible(); + }); +}); diff --git a/superset-frontend/src/explore/components/DataTablesPane/test/SamplesPane.test.tsx b/superset-frontend/src/explore/components/DataTablesPane/test/SamplesPane.test.tsx new file mode 100644 index 0000000000000..54c04c6003ba2 --- /dev/null +++ b/superset-frontend/src/explore/components/DataTablesPane/test/SamplesPane.test.tsx @@ -0,0 +1,106 @@ +/** + * 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 fetchMock from 'fetch-mock'; +import userEvent from '@testing-library/user-event'; +import { + render, + waitForElementToBeRemoved, +} from 'spec/helpers/testing-library'; +import { exploreActions } from 'src/explore/actions/exploreActions'; +import { promiseTimeout } from '@superset-ui/core'; +import { SamplesPane } from '../components'; +import { createSamplesPaneProps } from './fixture'; + +describe('SamplesPane', () => { + fetchMock.get('end:/api/v1/dataset/34/samples?force=false', { + result: { + data: [], + colnames: [], + coltypes: [], + }, + }); + + fetchMock.get('end:/api/v1/dataset/35/samples?force=true', { + result: { + data: [ + { __timestamp: 1230768000000, genre: 'Action' }, + { __timestamp: 1230768000010, genre: 'Horror' }, + ], + colnames: ['__timestamp', 'genre'], + coltypes: [2, 1], + }, + }); + + fetchMock.get('end:/api/v1/dataset/36/samples?force=false', 400); + + const setForceQuery = jest.spyOn(exploreActions, 'setForceQuery'); + + afterAll(() => { + fetchMock.reset(); + jest.resetAllMocks(); + }); + + test('render', async () => { + const props = createSamplesPaneProps({ datasourceId: 34 }); + const { findByText } = render(); + expect( + await findByText('No samples were returned for this dataset'), + ).toBeVisible(); + await promiseTimeout(() => { + expect(setForceQuery).toHaveBeenCalledTimes(0); + }, 10); + }); + + test('error response', async () => { + const props = createSamplesPaneProps({ + datasourceId: 36, + }); + const { findByText } = render(, { + useRedux: true, + }); + + expect(await findByText('Error: Bad Request')).toBeVisible(); + }); + + test('force query, render and search', async () => { + const props = createSamplesPaneProps({ + datasourceId: 35, + queryForce: true, + }); + const { queryByText, getByPlaceholderText } = render( + , + { + useRedux: true, + }, + ); + + await promiseTimeout(() => { + expect(setForceQuery).toHaveBeenCalledTimes(1); + }, 10); + expect(queryByText('2 rows')).toBeVisible(); + expect(queryByText('Action')).toBeVisible(); + expect(queryByText('Horror')).toBeVisible(); + + userEvent.type(getByPlaceholderText('Search'), 'hor'); + await waitForElementToBeRemoved(() => queryByText('Action')); + expect(queryByText('Horror')).toBeVisible(); + expect(queryByText('Action')).not.toBeInTheDocument(); + }); +}); diff --git a/superset-frontend/src/explore/components/DataTablesPane/test/fixture.tsx b/superset-frontend/src/explore/components/DataTablesPane/test/fixture.tsx new file mode 100644 index 0000000000000..d8428a227b165 --- /dev/null +++ b/superset-frontend/src/explore/components/DataTablesPane/test/fixture.tsx @@ -0,0 +1,119 @@ +/** + * 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 { DatasourceType } from '@superset-ui/core'; +import { exploreActions } from 'src/explore/actions/exploreActions'; +import { ChartStatus } from 'src/explore/types'; +import { + DataTablesPaneProps, + SamplesPaneProps, + ResultsPaneProps, +} from '../types'; + +const queryFormData = { + viz_type: 'heatmap', + datasource: '34__table', + slice_id: 456, + url_params: {}, + time_range: 'Last week', + all_columns_x: 'source', + all_columns_y: 'target', + metric: 'sum__value', + adhoc_filters: [], + row_limit: 10000, + linear_color_scheme: 'blue_white_yellow', + xscale_interval: null, + yscale_interval: null, + canvas_image_rendering: 'pixelated', + normalize_across: 'heatmap', + left_margin: 'auto', + bottom_margin: 'auto', + y_axis_bounds: [null, null], + y_axis_format: 'SMART_NUMBER', + show_perc: true, + sort_x_axis: 'alpha_asc', + sort_y_axis: 'alpha_asc', + extra_form_data: {}, +}; + +const datasource = { + id: 34, + name: '', + type: DatasourceType.Table, + columns: [], + metrics: [], + columnFormats: {}, + verboseMap: {}, +}; + +export const createDataTablesPaneProps = (sliceId: number) => + ({ + queryFormData: { + ...queryFormData, + slice_id: sliceId, + }, + datasource, + queryForce: false, + chartStatus: 'rendered' as ChartStatus, + onCollapseChange: jest.fn(), + actions: exploreActions, + } as DataTablesPaneProps); + +export const createSamplesPaneProps = ({ + datasourceId, + queryForce = false, + isRequest = true, +}: { + datasourceId: number; + queryForce?: boolean; + isRequest?: boolean; +}) => + ({ + isRequest, + datasource: { ...datasource, id: datasourceId }, + queryForce, + isVisible: true, + actions: exploreActions, + } as SamplesPaneProps); + +export const createResultsPaneOnDashboardProps = ({ + sliceId, + errorMessage, + vizType = 'table', + queryForce = false, + isRequest = true, +}: { + sliceId: number; + vizType?: string; + errorMessage?: React.ReactElement; + queryForce?: boolean; + isRequest?: boolean; +}) => + ({ + isRequest, + queryFormData: { + ...queryFormData, + slice_id: sliceId, + viz_type: vizType, + }, + queryForce, + isVisible: true, + actions: exploreActions, + errorMessage, + } as ResultsPaneProps); diff --git a/superset-frontend/src/explore/components/DataTablesPane/types.ts b/superset-frontend/src/explore/components/DataTablesPane/types.ts index f526536640c6e..4e6062ba4a256 100644 --- a/superset-frontend/src/explore/components/DataTablesPane/types.ts +++ b/superset-frontend/src/explore/components/DataTablesPane/types.ts @@ -25,6 +25,11 @@ import { import { ExploreActions } from 'src/explore/actions/exploreActions'; import { ChartStatus } from 'src/explore/types'; +export enum ResultTypes { + Results = 'results', + Samples = 'samples', +} + export interface DataTablesPaneProps { queryFormData: QueryFormData; datasource: Datasource; @@ -44,6 +49,8 @@ export interface ResultsPaneProps { errorMessage?: React.ReactElement; actions?: ExploreActions; dataSize?: number; + // reload OriginalFormattedTimeColumns from localStorage when isVisible is true + isVisible: boolean; } export interface SamplesPaneProps { @@ -52,6 +59,8 @@ export interface SamplesPaneProps { queryForce: boolean; actions?: ExploreActions; dataSize?: number; + // reload OriginalFormattedTimeColumns from localStorage when isVisible is true + isVisible: boolean; } export interface TableControlsProps { @@ -63,3 +72,17 @@ export interface TableControlsProps { columnTypes: GenericDataType[]; isLoading: boolean; } + +export interface QueryResultInterface { + colnames: string[]; + coltypes: GenericDataType[]; + data: Record[][]; +} + +export interface SingleQueryResultPaneProp extends QueryResultInterface { + // {datasource.id}__{datasource.type}, eg: 1__table + datasourceId: string; + dataSize?: number; + // reload OriginalFormattedTimeColumns from localStorage when isVisible is true + isVisible: boolean; +} diff --git a/superset-frontend/src/explore/components/DataTablesPane/utils.ts b/superset-frontend/src/explore/components/DataTablesPane/utils.ts new file mode 100644 index 0000000000000..c6394fb9b67f5 --- /dev/null +++ b/superset-frontend/src/explore/components/DataTablesPane/utils.ts @@ -0,0 +1,24 @@ +/** + * 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. + */ +const queryObjectCount = { + mixed_timeseries: 2, +}; + +export const getQueryCount = (vizType: string): number => + queryObjectCount?.[vizType] || 1;