This is an automated email from the ASF dual-hosted git repository.

yongjiezhao pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/superset.git


The following commit(s) were added to refs/heads/master by this push:
     new fd129873ce feat: multiple results pane on explore and dashboard 
(#20277)
fd129873ce is described below

commit fd129873ceeb74dc2e59d9b94ed1c9d006f1386c
Author: Yongjie Zhao <[email protected]>
AuthorDate: Thu Jun 9 09:11:34 2022 +0800

    feat: multiple results pane on explore and dashboard (#20277)
---
 .../components/SliceHeaderControls/index.tsx       |   5 +-
 .../explore/components/DataTableControl/index.tsx  |   9 +-
 .../components/DataTablesPane/DataTablesPane.tsx   |  56 +++++---
 .../components/ResultsPaneOnDashboard.tsx          |  69 +++++++++
 .../DataTablesPane/components/SamplesPane.tsx      |   3 +-
 .../components/SingleQueryResultPane.tsx           |  73 ++++++++++
 .../components/DataTablesPane/components/index.ts  |   3 +-
 .../{ResultsPane.tsx => useResultsPane.tsx}        | 119 +++++----------
 .../{ => test}/DataTablesPane.test.tsx             | 122 ++--------------
 .../test/ResultsPaneOnDashboard.test.tsx           | 160 +++++++++++++++++++++
 .../DataTablesPane/test/SamplesPane.test.tsx       | 106 ++++++++++++++
 .../components/DataTablesPane/test/fixture.tsx     | 119 +++++++++++++++
 .../src/explore/components/DataTablesPane/types.ts |  23 +++
 .../{components/index.ts => utils.ts}              |   9 +-
 14 files changed, 660 insertions(+), 216 deletions(-)

diff --git 
a/superset-frontend/src/dashboard/components/SliceHeaderControls/index.tsx 
b/superset-frontend/src/dashboard/components/SliceHeaderControls/index.tsx
index dcec62d88c..6b39e18449 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={
-                <ResultsPane
+                <ResultsPaneOnDashboard
                   queryFormData={this.props.formData}
                   queryForce={false}
                   dataSize={20}
                   isRequest
+                  isVisible
                 />
               }
               modalFooter={
diff --git 
a/superset-frontend/src/explore/components/DataTableControl/index.tsx 
b/superset-frontend/src/explore/components/DataTableControl/index.tsx
index fb8af865a3..fdc74d7bb4 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<boolean>(
-    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 99b7059c63..bfba9cf980 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 (
+        <Tabs.TabPane tab={t('Results')} key={ResultTypes.Results}>
+          {pane}
+        </Tabs.TabPane>
+      );
+    }
+    if (idx > 0) {
+      return (
+        <Tabs.TabPane
+          tab={t('Results %s', idx + 1)}
+          key={`${ResultTypes.Results} ${idx + 1}`}
+        >
+          {pane}
+        </Tabs.TabPane>
+      );
+    }
+    return null;
+  });
+
   return (
     <SouthPane data-test="some-purposeful-instance">
       <Tabs
@@ -195,22 +223,14 @@ export const DataTablesPane = ({
         activeKey={panelOpen ? activeTabKey : ''}
         onTabClick={handleTabClick}
       >
-        <Tabs.TabPane tab={t('Results')} key={ResultTypes.Results}>
-          <ResultsPane
-            errorMessage={errorMessage}
-            queryFormData={queryFormData}
-            queryForce={queryForce}
-            ownState={ownState}
-            isRequest={isRequest.results}
-            actions={actions}
-          />
-        </Tabs.TabPane>
+        {queryResultsPanes}
         <Tabs.TabPane tab={t('Samples')} key={ResultTypes.Samples}>
           <SamplesPane
             datasource={datasource}
             queryForce={queryForce}
             isRequest={isRequest.samples}
             actions={actions}
+            isVisible={ResultTypes.Samples === activeTabKey}
           />
         </Tabs.TabPane>
       </Tabs>
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 0000000000..3f27929f5c
--- /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 (
+        <Tabs.TabPane tab={t('Results')} key={ResultTypes.Results}>
+          {pane}
+        </Tabs.TabPane>
+      );
+    }
+
+    return (
+      <Tabs.TabPane
+        tab={t('Results %s', idx + 1)}
+        key={`${ResultTypes.Results} ${idx + 1}`}
+      >
+        {pane}
+      </Tabs.TabPane>
+    );
+  });
+
+  return <Tabs fullWidth={false}> {panes} </Tabs>;
+};
diff --git 
a/superset-frontend/src/explore/components/DataTablesPane/components/SamplesPane.tsx
 
b/superset-frontend/src/explore/components/DataTablesPane/components/SamplesPane.tsx
index 1997acf596..8b1137334b 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<Record<string, any>[][]>([]);
@@ -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 0000000000..27d312cc3c
--- /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 (
+    <>
+      <TableControls
+        data={filteredData}
+        columnNames={colnames}
+        columnTypes={coltypes}
+        datasourceId={datasourceId}
+        onInputChange={input => setFilterText(input)}
+        isLoading={false}
+      />
+      <TableView
+        columns={columns}
+        data={filteredData}
+        pageSize={dataSize}
+        noDataText={t('No results')}
+        emptyWrapperType={EmptyWrapperType.Small}
+        className="table-condensed"
+        isPaginationSticky
+        showRowCount={false}
+        small
+      />
+    </>
+  );
+};
diff --git 
a/superset-frontend/src/explore/components/DataTablesPane/components/index.ts 
b/superset-frontend/src/explore/components/DataTablesPane/components/index.ts
index 41623cb572..e5762494c5 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 d69a244430..20e53df849 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<Record<string, any>[][]>([]);
-  const [colnames, setColnames] = useState<string[]>([]);
-  const [coltypes, setColtypes] = useState<GenericDataType[]>([]);
+}: ResultsPaneProps): React.ReactElement[] => {
+  const [resultResp, setResultResp] = useState<QueryResultInterface[]>([]);
   const [isLoading, setIsLoading] = useState<boolean>(true);
   const [responseError, setResponseError] = useState<string>('');
+  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 <Loading />;
+    return Array(queryCount).fill(<Loading />);
   }
 
   if (errorMessage) {
     const title = t('Run a query to display results');
-    return <EmptyStateMedium image="document.svg" title={title} />;
+    return Array(queryCount).fill(
+      <EmptyStateMedium image="document.svg" title={title} />,
+    );
   }
 
   if (responseError) {
-    return (
+    const err = (
       <>
         <TableControls
-          data={filteredData}
-          columnNames={colnames}
-          columnTypes={coltypes}
-          datasourceId={queryFormData?.datasource}
-          onInputChange={input => setFilterText(input)}
-          isLoading={isLoading}
+          data={[]}
+          columnNames={[]}
+          columnTypes={[]}
+          datasourceId={queryFormData.datasource}
+          onInputChange={() => {}}
+          isLoading={false}
         />
         <Error>{responseError}</Error>
       </>
     );
+    return Array(queryCount).fill(err);
   }
 
-  if (data.length === 0) {
+  if (resultResp.length === 0) {
     const title = t('No results were returned for this query');
-    return <EmptyStateMedium image="document.svg" title={title} />;
+    return Array(queryCount).fill(
+      <EmptyStateMedium image="document.svg" title={title} />,
+    );
   }
 
-  return (
-    <>
-      <TableControls
-        data={filteredData}
-        columnNames={colnames}
-        columnTypes={coltypes}
-        datasourceId={queryFormData?.datasource}
-        onInputChange={input => setFilterText(input)}
-        isLoading={isLoading}
-      />
-      <TableView
-        columns={columns}
-        data={filteredData}
-        pageSize={dataSize}
-        noDataText={t('No results')}
-        emptyWrapperType={EmptyWrapperType.Small}
-        className="table-condensed"
-        isPaginationSticky
-        showRowCount={false}
-        small
-      />
-    </>
-  );
+  return resultResp.map((result, idx) => (
+    <SingleQueryResultPane
+      data={result.data}
+      colnames={result.colnames}
+      coltypes={result.coltypes}
+      dataSize={dataSize}
+      datasourceId={queryFormData.datasource}
+      key={idx}
+      isVisible={isVisible}
+    />
+  ));
 };
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 57d599ee82..c5d9d0c7bb 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(<DataTablesPane {...props} />, { 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(<DataTablesPane {...props} />, {
       useRedux: true,
     });
@@ -112,7 +63,7 @@ describe('DataTablesPane', () => {
   });
 
   test('Should show tabs: View results', async () => {
-    const props = createProps();
+    const props = createDataTablesPaneProps(0);
     render(<DataTablesPane {...props} />, {
       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(<DataTablesPane {...props} />, {
       useRedux: true,
     });
@@ -146,31 +96,10 @@ describe('DataTablesPane', () => {
       },
     );
     const copyToClipboardSpy = jest.spyOn(copyUtils, 'default');
-    const props = createProps();
-    render(
-      <DataTablesPane
-        {...{
-          ...props,
-          chartStatus: 'rendered',
-          queriesResponse: [
-            {
-              colnames: ['__timestamp', 'genre'],
-              coltypes: [2, 1],
-            },
-          ],
-        }}
-      />,
-      {
-        useRedux: true,
-        initialState: {
-          explore: {
-            originalFormattedTimeColumns: {
-              '34__table': ['__timestamp'],
-            },
-          },
-        },
-      },
-    );
+    const props = createDataTablesPaneProps(456);
+    render(<DataTablesPane {...props} />, {
+      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(
-      <DataTablesPane
-        {...{
-          ...props,
-          chartStatus: 'rendered',
-          queriesResponse: [
-            {
-              colnames: ['__timestamp', 'genre'],
-              coltypes: [2, 1],
-            },
-          ],
-        }}
-      />,
-      {
-        useRedux: true,
-        initialState: {
-          explore: {
-            originalFormattedTimeColumns: {
-              '34__table': ['__timestamp'],
-            },
-          },
-        },
-      },
-    );
+    const props = createDataTablesPaneProps(789);
+    render(<DataTablesPane {...props} />, {
+      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 0000000000..19980ff711
--- /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(<ResultsPaneOnDashboard {...props} />, {
+      useRedux: true,
+    });
+    expect(
+      await findByText('No results were returned for this query'),
+    ).toBeVisible();
+  });
+
+  test('render errorMessage', async () => {
+    const props = createResultsPaneOnDashboardProps({
+      sliceId: 121,
+      errorMessage: <p>error</p>,
+    });
+    const { findByText } = render(<ResultsPaneOnDashboard {...props} />, {
+      useRedux: true,
+    });
+    expect(await findByText('Run a query to display results')).toBeVisible();
+  });
+
+  test('error response', async () => {
+    const props = createResultsPaneOnDashboardProps({
+      sliceId: 169,
+    });
+    const { findByText } = render(<ResultsPaneOnDashboard {...props} />, {
+      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(
+      <ResultsPaneOnDashboard {...props} />,
+      {
+        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(<ResultsPaneOnDashboard {...props} />, {
+      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 0000000000..54c04c6003
--- /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(<SamplesPane {...props} />);
+    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(<SamplesPane {...props} />, {
+      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(
+      <SamplesPane {...props} />,
+      {
+        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 0000000000..d8428a227b
--- /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 f526536640..4e6062ba4a 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<string, any>[][];
+}
+
+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/components/index.ts 
b/superset-frontend/src/explore/components/DataTablesPane/utils.ts
similarity index 83%
copy from 
superset-frontend/src/explore/components/DataTablesPane/components/index.ts
copy to superset-frontend/src/explore/components/DataTablesPane/utils.ts
index 41623cb572..c6394fb9b6 100644
--- 
a/superset-frontend/src/explore/components/DataTablesPane/components/index.ts
+++ b/superset-frontend/src/explore/components/DataTablesPane/utils.ts
@@ -16,6 +16,9 @@
  * specific language governing permissions and limitations
  * under the License.
  */
-export { ResultsPane } from './ResultsPane';
-export { SamplesPane } from './SamplesPane';
-export { TableControls, TableControlsWrapper } from './DataTableControls';
+const queryObjectCount = {
+  mixed_timeseries: 2,
+};
+
+export const getQueryCount = (vizType: string): number =>
+  queryObjectCount?.[vizType] || 1;

Reply via email to