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;