You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@superset.apache.org by mi...@apache.org on 2023/12/18 14:10:38 UTC

(superset) branch 3.1 updated (2dc29cee9a -> 9632014402)

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

michaelsmolina pushed a change to branch 3.1
in repository https://gitbox.apache.org/repos/asf/superset.git


    from 2dc29cee9a chore: Adds 3.1.0 RC2 data to CHANGELOG.md and UPDATING.md
     new 0ac833d0c5 fix(plugin-chart-echarts): undefined bounds for bubble chart (#26243)
     new 3d7b827d79 chore: improve CSP add base uri restriction (#26251)
     new dbc779f30a fix: Stacked charts with numerical columns (#26264)
     new 4d0404119c fix(plugin-chart-echarts): use scale for truncating x-axis (#26269)
     new 103d23781b fix: Cannot expand initially hidden SQL Lab tab (#26279)
     new 9632014402 fix: Revert "fix(sqllab): flaky json explore modal due to over-rendering (#26156)" (#26284)

The 6 revisions listed above as "new" are entirely new to this
repository and will be described in separate emails.  The revisions
listed as "add" were already present in the repository and have only
been added to this reference.


Summary of changes:
 .../packages/superset-ui-core/src/chart/index.ts   |   2 +-
 .../plugin-chart-echarts/src/Bubble/constants.ts   |   1 +
 .../src/Bubble/transformProps.ts                   |   4 +-
 .../src/MixedTimeseries/controlPanel.tsx           |   4 +
 .../src/MixedTimeseries/transformProps.ts          |  54 ++--
 .../src/MixedTimeseries/types.ts                   |   1 +
 .../src/Timeseries/transformProps.ts               |  10 +-
 .../plugin-chart-echarts/src/utils/controls.ts     |   1 -
 .../plugin-chart-echarts/src/utils/series.ts       |  47 ++-
 .../test/Bubble/transformProps.test.ts             |  48 ++-
 .../plugin-chart-echarts/test/utils/series.test.ts | 112 ++++++-
 .../SqlLab/components/QueryAutoRefresh/index.tsx   |   2 +-
 .../components/QueryHistory/QueryHistory.test.tsx  |   5 +-
 .../src/SqlLab/components/QueryHistory/index.tsx   |  29 +-
 .../src/SqlLab/components/QueryTable/index.tsx     |   3 +-
 .../SqlLab/components/ResultSet/ResultSet.test.tsx | 333 +++++----------------
 .../src/SqlLab/components/ResultSet/index.tsx      |  52 +---
 .../SqlLab/components/SouthPane/Results.test.tsx   | 135 ---------
 .../src/SqlLab/components/SouthPane/Results.tsx    | 106 -------
 .../SqlLab/components/SouthPane/SouthPane.test.tsx |  81 ++---
 .../src/SqlLab/components/SouthPane/index.tsx      | 174 +++++++----
 .../SqlLab/components/SqlEditor/SqlEditor.test.tsx |   7 +-
 .../src/SqlLab/components/SqlEditor/index.tsx      |   6 +-
 superset-frontend/src/SqlLab/reducers/sqlLab.js    |  17 +-
 .../src/SqlLab/reducers/sqlLab.test.js             |   5 +-
 superset/config.py                                 |   2 +
 26 files changed, 512 insertions(+), 729 deletions(-)
 delete mode 100644 superset-frontend/src/SqlLab/components/SouthPane/Results.test.tsx
 delete mode 100644 superset-frontend/src/SqlLab/components/SouthPane/Results.tsx


(superset) 03/06: fix: Stacked charts with numerical columns (#26264)

Posted by mi...@apache.org.
This is an automated email from the ASF dual-hosted git repository.

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

commit dbc779f30adcdb185d27e8e319fb06a21204d79f
Author: Michael S. Molina <70...@users.noreply.github.com>
AuthorDate: Wed Dec 13 17:10:51 2023 -0300

    fix: Stacked charts with numerical columns (#26264)
    
    (cherry picked from commit 429e2a33c3ac5a4b035e0cb113bc6e1e63a39e4c)
---
 .../src/MixedTimeseries/transformProps.ts                     |  2 +-
 .../plugin-chart-echarts/src/Timeseries/transformProps.ts     |  2 +-
 .../plugins/plugin-chart-echarts/src/utils/series.ts          |  7 +++++--
 .../plugins/plugin-chart-echarts/test/utils/series.test.ts    | 11 +++++++----
 4 files changed, 14 insertions(+), 8 deletions(-)

diff --git a/superset-frontend/plugins/plugin-chart-echarts/src/MixedTimeseries/transformProps.ts b/superset-frontend/plugins/plugin-chart-echarts/src/MixedTimeseries/transformProps.ts
index 8bc01582af..f924ad6f9b 100644
--- a/superset-frontend/plugins/plugin-chart-echarts/src/MixedTimeseries/transformProps.ts
+++ b/superset-frontend/plugins/plugin-chart-echarts/src/MixedTimeseries/transformProps.ts
@@ -223,7 +223,7 @@ export default function transformProps(
 
   const dataTypes = getColtypesMapping(queriesData[0]);
   const xAxisDataType = dataTypes?.[xAxisLabel] ?? dataTypes?.[xAxisOrig];
-  const xAxisType = getAxisType(xAxisDataType);
+  const xAxisType = getAxisType(stack, xAxisDataType);
   const series: SeriesOption[] = [];
   const formatter = contributionMode
     ? getNumberFormatter(',.0%')
diff --git a/superset-frontend/plugins/plugin-chart-echarts/src/Timeseries/transformProps.ts b/superset-frontend/plugins/plugin-chart-echarts/src/Timeseries/transformProps.ts
index e42ac183b6..8dd9966484 100644
--- a/superset-frontend/plugins/plugin-chart-echarts/src/Timeseries/transformProps.ts
+++ b/superset-frontend/plugins/plugin-chart-echarts/src/Timeseries/transformProps.ts
@@ -247,7 +247,7 @@ export default function transformProps(
   const isAreaExpand = stack === StackControlsValue.Expand;
   const xAxisDataType = dataTypes?.[xAxisLabel] ?? dataTypes?.[xAxisOrig];
 
-  const xAxisType = getAxisType(xAxisDataType);
+  const xAxisType = getAxisType(stack, xAxisDataType);
   const series: SeriesOption[] = [];
 
   const forcePercentFormatter = Boolean(contributionMode || isAreaExpand);
diff --git a/superset-frontend/plugins/plugin-chart-echarts/src/utils/series.ts b/superset-frontend/plugins/plugin-chart-echarts/src/utils/series.ts
index aa353f66d1..69c0ccbe1b 100644
--- a/superset-frontend/plugins/plugin-chart-echarts/src/utils/series.ts
+++ b/superset-frontend/plugins/plugin-chart-echarts/src/utils/series.ts
@@ -508,11 +508,14 @@ export function sanitizeHtml(text: string): string {
   return format.encodeHTML(text);
 }
 
-export function getAxisType(dataType?: GenericDataType): AxisType {
+export function getAxisType(
+  stack: StackType,
+  dataType?: GenericDataType,
+): AxisType {
   if (dataType === GenericDataType.TEMPORAL) {
     return AxisType.time;
   }
-  if (dataType === GenericDataType.NUMERIC) {
+  if (dataType === GenericDataType.NUMERIC && !stack) {
     return AxisType.value;
   }
   return AxisType.category;
diff --git a/superset-frontend/plugins/plugin-chart-echarts/test/utils/series.test.ts b/superset-frontend/plugins/plugin-chart-echarts/test/utils/series.test.ts
index b445dceabb..b309bf6f3c 100644
--- a/superset-frontend/plugins/plugin-chart-echarts/test/utils/series.test.ts
+++ b/superset-frontend/plugins/plugin-chart-echarts/test/utils/series.test.ts
@@ -875,10 +875,13 @@ test('calculateLowerLogTick', () => {
 });
 
 test('getAxisType', () => {
-  expect(getAxisType(GenericDataType.TEMPORAL)).toEqual(AxisType.time);
-  expect(getAxisType(GenericDataType.NUMERIC)).toEqual(AxisType.value);
-  expect(getAxisType(GenericDataType.BOOLEAN)).toEqual(AxisType.category);
-  expect(getAxisType(GenericDataType.STRING)).toEqual(AxisType.category);
+  expect(getAxisType(false, GenericDataType.TEMPORAL)).toEqual(AxisType.time);
+  expect(getAxisType(false, GenericDataType.NUMERIC)).toEqual(AxisType.value);
+  expect(getAxisType(true, GenericDataType.NUMERIC)).toEqual(AxisType.category);
+  expect(getAxisType(false, GenericDataType.BOOLEAN)).toEqual(
+    AxisType.category,
+  );
+  expect(getAxisType(false, GenericDataType.STRING)).toEqual(AxisType.category);
 });
 
 test('getMinAndMaxFromBounds returns empty object when not truncating', () => {


(superset) 05/06: fix: Cannot expand initially hidden SQL Lab tab (#26279)

Posted by mi...@apache.org.
This is an automated email from the ASF dual-hosted git repository.

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

commit 103d23781b8d5206c1e8e62f09836a736bb74380
Author: Michael S. Molina <70...@users.noreply.github.com>
AuthorDate: Thu Dec 14 16:48:07 2023 -0300

    fix: Cannot expand initially hidden SQL Lab tab (#26279)
    
    (cherry picked from commit aa3c3c5aaa0d9fa1769ca310c9e944e86695d7db)
---
 superset-frontend/src/SqlLab/components/SqlEditor/index.tsx | 6 ++++--
 1 file changed, 4 insertions(+), 2 deletions(-)

diff --git a/superset-frontend/src/SqlLab/components/SqlEditor/index.tsx b/superset-frontend/src/SqlLab/components/SqlEditor/index.tsx
index 73941fbc79..0881434487 100644
--- a/superset-frontend/src/SqlLab/components/SqlEditor/index.tsx
+++ b/superset-frontend/src/SqlLab/components/SqlEditor/index.tsx
@@ -93,7 +93,7 @@ import {
 } from 'src/utils/localStorageHelpers';
 import { EmptyStateBig } from 'src/components/EmptyState';
 import getBootstrapData from 'src/utils/getBootstrapData';
-import { isEmpty } from 'lodash';
+import { isBoolean, isEmpty } from 'lodash';
 import TemplateParamsEditor from '../TemplateParamsEditor';
 import SouthPane from '../SouthPane';
 import SaveQuery, { QueryPayload } from '../SaveQuery';
@@ -255,7 +255,9 @@ const SqlEditor: React.FC<Props> = ({
     if (unsavedQueryEditor?.id === queryEditor.id) {
       dbId = unsavedQueryEditor.dbId || dbId;
       latestQueryId = unsavedQueryEditor.latestQueryId || latestQueryId;
-      hideLeftBar = unsavedQueryEditor.hideLeftBar || hideLeftBar;
+      hideLeftBar = isBoolean(unsavedQueryEditor.hideLeftBar)
+        ? unsavedQueryEditor.hideLeftBar
+        : hideLeftBar;
     }
     return {
       database: databases[dbId || ''],


(superset) 02/06: chore: improve CSP add base uri restriction (#26251)

Posted by mi...@apache.org.
This is an automated email from the ASF dual-hosted git repository.

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

commit 3d7b827d7986e4e70e967b2274649625c8c0dfc7
Author: Daniel Vaz Gaspar <da...@gmail.com>
AuthorDate: Wed Dec 13 11:45:14 2023 +0000

    chore: improve CSP add base uri restriction (#26251)
    
    (cherry picked from commit 578a899152719415c65c24055f4378b838ded435)
---
 superset/config.py | 2 ++
 1 file changed, 2 insertions(+)

diff --git a/superset/config.py b/superset/config.py
index 98f87e6f02..ca801442d9 100644
--- a/superset/config.py
+++ b/superset/config.py
@@ -1425,6 +1425,7 @@ TALISMAN_ENABLED = utils.cast_to_boolean(os.environ.get("TALISMAN_ENABLED", True
 # If you want Talisman, how do you want it configured??
 TALISMAN_CONFIG = {
     "content_security_policy": {
+        "base-uri": ["'self'"],
         "default-src": ["'self'"],
         "img-src": ["'self'", "blob:", "data:"],
         "worker-src": ["'self'", "blob:"],
@@ -1447,6 +1448,7 @@ TALISMAN_CONFIG = {
 # React requires `eval` to work correctly in dev mode
 TALISMAN_DEV_CONFIG = {
     "content_security_policy": {
+        "base-uri": ["'self'"],
         "default-src": ["'self'"],
         "img-src": ["'self'", "blob:", "data:"],
         "worker-src": ["'self'", "blob:"],


(superset) 06/06: fix: Revert "fix(sqllab): flaky json explore modal due to over-rendering (#26156)" (#26284)

Posted by mi...@apache.org.
This is an automated email from the ASF dual-hosted git repository.

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

commit 9632014402ea2f11e512ff862659fcc55de783a9
Author: JUST.in DO IT <ju...@airbnb.com>
AuthorDate: Fri Dec 15 15:05:00 2023 -0800

    fix: Revert "fix(sqllab): flaky json explore modal due to over-rendering (#26156)" (#26284)
    
    (cherry picked from commit 8450cca9989eed29b96f0bf9f963ab07a3ee434e)
---
 .../SqlLab/components/QueryAutoRefresh/index.tsx   |   2 +-
 .../components/QueryHistory/QueryHistory.test.tsx  |   5 +-
 .../src/SqlLab/components/QueryHistory/index.tsx   |  29 +-
 .../src/SqlLab/components/QueryTable/index.tsx     |   3 +-
 .../SqlLab/components/ResultSet/ResultSet.test.tsx | 333 +++++----------------
 .../src/SqlLab/components/ResultSet/index.tsx      |  52 +---
 .../SqlLab/components/SouthPane/Results.test.tsx   | 135 ---------
 .../src/SqlLab/components/SouthPane/Results.tsx    | 106 -------
 .../SqlLab/components/SouthPane/SouthPane.test.tsx |  81 ++---
 .../src/SqlLab/components/SouthPane/index.tsx      | 174 +++++++----
 .../SqlLab/components/SqlEditor/SqlEditor.test.tsx |   7 +-
 superset-frontend/src/SqlLab/reducers/sqlLab.js    |  17 +-
 .../src/SqlLab/reducers/sqlLab.test.js             |   5 +-
 13 files changed, 274 insertions(+), 675 deletions(-)

diff --git a/superset-frontend/src/SqlLab/components/QueryAutoRefresh/index.tsx b/superset-frontend/src/SqlLab/components/QueryAutoRefresh/index.tsx
index a2ffcf85cb..f4808f52fd 100644
--- a/superset-frontend/src/SqlLab/components/QueryAutoRefresh/index.tsx
+++ b/superset-frontend/src/SqlLab/components/QueryAutoRefresh/index.tsx
@@ -45,7 +45,7 @@ export interface QueryAutoRefreshProps {
 
 // returns true if the Query.state matches one of the specifc values indicating the query is still processing on server
 export const isQueryRunning = (q: Query): boolean =>
-  runningQueryStateList.includes(q?.state) && !q?.resultsKey;
+  runningQueryStateList.includes(q?.state);
 
 // returns true if at least one query is running and within the max age to poll timeframe
 export const shouldCheckForQueries = (queryList: QueryDictionary): boolean => {
diff --git a/superset-frontend/src/SqlLab/components/QueryHistory/QueryHistory.test.tsx b/superset-frontend/src/SqlLab/components/QueryHistory/QueryHistory.test.tsx
index ad1881b5d9..6fd84a0d2a 100644
--- a/superset-frontend/src/SqlLab/components/QueryHistory/QueryHistory.test.tsx
+++ b/superset-frontend/src/SqlLab/components/QueryHistory/QueryHistory.test.tsx
@@ -19,10 +19,9 @@
 import React from 'react';
 import { render, screen } from 'spec/helpers/testing-library';
 import QueryHistory from 'src/SqlLab/components/QueryHistory';
-import { initialState } from 'src/SqlLab/fixtures';
 
 const mockedProps = {
-  queryEditorId: 123,
+  queries: [],
   displayLimit: 1000,
   latestQueryId: 'yhMUZCGb',
 };
@@ -33,7 +32,7 @@ const setup = (overrides = {}) => (
 
 describe('QueryHistory', () => {
   it('Renders an empty state for query history', () => {
-    render(setup(), { useRedux: true, initialState });
+    render(setup());
 
     const emptyStateText = screen.getByText(
       /run a query to display query history/i,
diff --git a/superset-frontend/src/SqlLab/components/QueryHistory/index.tsx b/superset-frontend/src/SqlLab/components/QueryHistory/index.tsx
index 311a125d55..cab1160144 100644
--- a/superset-frontend/src/SqlLab/components/QueryHistory/index.tsx
+++ b/superset-frontend/src/SqlLab/components/QueryHistory/index.tsx
@@ -16,15 +16,13 @@
  * specific language governing permissions and limitations
  * under the License.
  */
-import React, { useMemo } from 'react';
-import { shallowEqual, useSelector } from 'react-redux';
+import React from 'react';
 import { EmptyStateMedium } from 'src/components/EmptyState';
-import { t, styled } from '@superset-ui/core';
+import { t, styled, QueryResponse } from '@superset-ui/core';
 import QueryTable from 'src/SqlLab/components/QueryTable';
-import { SqlLabRootState } from 'src/SqlLab/types';
 
 interface QueryHistoryProps {
-  queryEditorId: string | number;
+  queries: QueryResponse[];
   displayLimit: number;
   latestQueryId: string | undefined;
 }
@@ -41,23 +39,11 @@ const StyledEmptyStateWrapper = styled.div`
 `;
 
 const QueryHistory = ({
-  queryEditorId,
+  queries,
   displayLimit,
   latestQueryId,
-}: QueryHistoryProps) => {
-  const queries = useSelector(
-    ({ sqlLab: { queries } }: SqlLabRootState) => queries,
-    shallowEqual,
-  );
-  const editorQueries = useMemo(
-    () =>
-      Object.values(queries).filter(
-        ({ sqlEditorId }) => String(sqlEditorId) === String(queryEditorId),
-      ),
-    [queries, queryEditorId],
-  );
-
-  return editorQueries.length > 0 ? (
+}: QueryHistoryProps) =>
+  queries.length > 0 ? (
     <QueryTable
       columns={[
         'state',
@@ -69,7 +55,7 @@ const QueryHistory = ({
         'results',
         'actions',
       ]}
-      queries={editorQueries}
+      queries={queries}
       displayLimit={displayLimit}
       latestQueryId={latestQueryId}
     />
@@ -81,6 +67,5 @@ const QueryHistory = ({
       />
     </StyledEmptyStateWrapper>
   );
-};
 
 export default QueryHistory;
diff --git a/superset-frontend/src/SqlLab/components/QueryTable/index.tsx b/superset-frontend/src/SqlLab/components/QueryTable/index.tsx
index 3282a939ef..5dc8a43c19 100644
--- a/superset-frontend/src/SqlLab/components/QueryTable/index.tsx
+++ b/superset-frontend/src/SqlLab/components/QueryTable/index.tsx
@@ -251,7 +251,8 @@ const QueryTable = ({
               modalBody={
                 <ResultSet
                   showSql
-                  queryId={query.id}
+                  user={user}
+                  query={query}
                   height={400}
                   displayLimit={displayLimit}
                   defaultQueryLimit={1000}
diff --git a/superset-frontend/src/SqlLab/components/ResultSet/ResultSet.test.tsx b/superset-frontend/src/SqlLab/components/ResultSet/ResultSet.test.tsx
index e5844fed5c..d823c586f7 100644
--- a/superset-frontend/src/SqlLab/components/ResultSet/ResultSet.test.tsx
+++ b/superset-frontend/src/SqlLab/components/ResultSet/ResultSet.test.tsx
@@ -37,91 +37,65 @@ import {
 
 const mockedProps = {
   cache: true,
-  queryId: queries[0].id,
+  query: queries[0],
   height: 140,
   database: { allows_virtual_table_explore: true },
-  displayLimit: 1000,
+  user,
   defaultQueryLimit: 1000,
 };
-const stoppedQueryState = {
-  ...initialState,
-  sqlLab: {
-    ...initialState.sqlLab,
-    queries: {
-      [stoppedQuery.id]: stoppedQuery,
-    },
-  },
-};
-const runningQueryState = {
-  ...initialState,
-  sqlLab: {
-    ...initialState.sqlLab,
-    queries: {
-      [runningQuery.id]: runningQuery,
-    },
-  },
-};
-const fetchingQueryState = {
-  ...initialState,
-  sqlLab: {
-    ...initialState.sqlLab,
-    queries: {
-      [mockedProps.queryId]: {
-        dbId: 1,
-        cached: false,
-        ctas: false,
-        id: 'ryhHUZCGb',
-        progress: 100,
-        state: 'fetching',
-        startDttm: Date.now() - 500,
-      },
-    },
+const stoppedQueryProps = { ...mockedProps, query: stoppedQuery };
+const runningQueryProps = { ...mockedProps, query: runningQuery };
+const fetchingQueryProps = {
+  ...mockedProps,
+  query: {
+    dbId: 1,
+    cached: false,
+    ctas: false,
+    id: 'ryhHUZCGb',
+    progress: 100,
+    state: 'fetching',
+    startDttm: Date.now() - 500,
   },
 };
-const cachedQueryState = {
-  ...initialState,
-  sqlLab: {
-    ...initialState.sqlLab,
-    queries: {
-      [cachedQuery.id]: cachedQuery,
-    },
-  },
+const cachedQueryProps = { ...mockedProps, query: cachedQuery };
+const failedQueryWithErrorMessageProps = {
+  ...mockedProps,
+  query: failedQueryWithErrorMessage,
 };
-const failedQueryWithErrorMessageState = {
-  ...initialState,
-  sqlLab: {
-    ...initialState.sqlLab,
-    queries: {
-      [failedQueryWithErrorMessage.id]: failedQueryWithErrorMessage,
-    },
-  },
+const failedQueryWithErrorsProps = {
+  ...mockedProps,
+  query: failedQueryWithErrors,
 };
-const failedQueryWithErrorsState = {
-  ...initialState,
-  sqlLab: {
-    ...initialState.sqlLab,
-    queries: {
-      [failedQueryWithErrors.id]: failedQueryWithErrors,
+const newProps = {
+  query: {
+    cached: false,
+    resultsKey: 'new key',
+    results: {
+      data: [{ a: 1 }],
     },
   },
 };
-
-const newProps = {
-  displayLimit: 1001,
-};
 const asyncQueryProps = {
   ...mockedProps,
   database: { allow_run_async: true },
 };
-
-const reRunQueryEndpoint = 'glob:*/api/v1/sqllab/execute/';
+const asyncRefetchDataPreviewProps = {
+  ...asyncQueryProps,
+  query: {
+    state: 'success',
+    results: undefined,
+    isDataPreview: true,
+  },
+};
+const asyncRefetchResultsTableProps = {
+  ...asyncQueryProps,
+  query: {
+    state: 'success',
+    results: undefined,
+    resultsKey: 'async results key',
+  },
+};
 fetchMock.get('glob:*/api/v1/dataset/?*', { result: [] });
-fetchMock.post(reRunQueryEndpoint, { result: [] });
-fetchMock.get('glob:*/api/v1/sqllab/results/*', { result: [] });
-
-beforeEach(() => {
-  fetchMock.resetHistory();
-});
 
 const middlewares = [thunk];
 const mockStore = configureStore(middlewares);
@@ -133,47 +107,25 @@ const setup = (props?: any, store?: Store) =>
 
 describe('ResultSet', () => {
   test('renders a Table', async () => {
-    const { getByTestId } = setup(
-      mockedProps,
-      mockStore({
-        ...initialState,
-        user,
-        sqlLab: {
-          ...initialState.sqlLab,
-          queries: {
-            [queries[0].id]: queries[0],
-          },
-        },
-      }),
-    );
+    const { getByTestId } = setup(mockedProps, mockStore(initialState));
     const table = getByTestId('table-container');
     expect(table).toBeInTheDocument();
   });
 
   test('should render success query', async () => {
-    const query = queries[0];
     const { queryAllByText, getByTestId } = setup(
       mockedProps,
-      mockStore({
-        ...initialState,
-        user,
-        sqlLab: {
-          ...initialState.sqlLab,
-          queries: {
-            [query.id]: query,
-          },
-        },
-      }),
+      mockStore(initialState),
     );
 
     const table = getByTestId('table-container');
     expect(table).toBeInTheDocument();
 
     const firstColumn = queryAllByText(
-      query.results?.columns[0].column_name ?? '',
+      mockedProps.query.results?.columns[0].column_name ?? '',
     )[0];
     const secondColumn = queryAllByText(
-      query.results?.columns[1].column_name ?? '',
+      mockedProps.query.results?.columns[1].column_name ?? '',
     )[0];
     expect(firstColumn).toBeInTheDocument();
     expect(secondColumn).toBeInTheDocument();
@@ -183,24 +135,12 @@ describe('ResultSet', () => {
   });
 
   test('should render empty results', async () => {
-    const query = {
-      ...queries[0],
-      results: { data: [] },
+    const props = {
+      ...mockedProps,
+      query: { ...mockedProps.query, results: { data: [] } },
     };
     await waitFor(() => {
-      setup(
-        mockedProps,
-        mockStore({
-          ...initialState,
-          user,
-          sqlLab: {
-            ...initialState.sqlLab,
-            queries: {
-              [query.id]: query,
-            },
-          },
-        }),
-      );
+      setup(props, mockStore(initialState));
     });
 
     const alert = screen.getByRole('alert');
@@ -209,70 +149,42 @@ describe('ResultSet', () => {
   });
 
   test('should call reRunQuery if timed out', async () => {
-    const query = {
-      ...queries[0],
-      errorMessage: 'Your session timed out',
+    const store = mockStore(initialState);
+    const propsWithError = {
+      ...mockedProps,
+      query: { ...queries[0], errorMessage: 'Your session timed out' },
     };
-    const store = mockStore({
-      ...initialState,
-      user,
-      sqlLab: {
-        ...initialState.sqlLab,
-        queries: {
-          [query.id]: query,
-        },
-      },
-    });
 
-    expect(fetchMock.calls(reRunQueryEndpoint)).toHaveLength(0);
-    setup(mockedProps, store);
+    setup(propsWithError, store);
     expect(store.getActions()).toHaveLength(1);
     expect(store.getActions()[0].query.errorMessage).toEqual(
       'Your session timed out',
     );
     expect(store.getActions()[0].type).toEqual('START_QUERY');
-    await waitFor(() =>
-      expect(fetchMock.calls(reRunQueryEndpoint)).toHaveLength(1),
-    );
   });
 
   test('should not call reRunQuery if no error', async () => {
-    const query = queries[0];
-    const store = mockStore({
-      ...initialState,
-      user,
-      sqlLab: {
-        ...initialState.sqlLab,
-        queries: {
-          [query.id]: query,
-        },
-      },
-    });
+    const store = mockStore(initialState);
     setup(mockedProps, store);
     expect(store.getActions()).toEqual([]);
-    expect(fetchMock.calls(reRunQueryEndpoint)).toHaveLength(0);
   });
 
   test('should render cached query', async () => {
-    const store = mockStore(cachedQueryState);
-    const { rerender } = setup(
-      { ...mockedProps, queryId: cachedQuery.id },
-      store,
-    );
+    const store = mockStore(initialState);
+    const { rerender } = setup(cachedQueryProps, store);
 
     // @ts-ignore
-    rerender(<ResultSet {...mockedProps} {...newProps} />);
-    expect(store.getActions()).toHaveLength(1);
-    expect(store.getActions()[0].query.results).toEqual(cachedQuery.results);
+    rerender(<ResultSet {...newProps} />);
+    expect(store.getActions()).toHaveLength(2);
+    expect(store.getActions()[0].query.results).toEqual(
+      cachedQueryProps.query.results,
+    );
     expect(store.getActions()[0].type).toEqual('CLEAR_QUERY_RESULTS');
   });
 
   test('should render stopped query', async () => {
     await waitFor(() => {
-      setup(
-        { ...mockedProps, queryId: stoppedQuery.id },
-        mockStore(stoppedQueryState),
-      );
+      setup(stoppedQueryProps, mockStore(initialState));
     });
 
     const alert = screen.getByRole('alert');
@@ -280,18 +192,15 @@ describe('ResultSet', () => {
   });
 
   test('should render running/pending/fetching query', async () => {
-    const { getByTestId } = setup(
-      { ...mockedProps, queryId: runningQuery.id },
-      mockStore(runningQueryState),
-    );
+    const { getByTestId } = setup(runningQueryProps, mockStore(initialState));
     const progressBar = getByTestId('progress-bar');
     expect(progressBar).toBeInTheDocument();
   });
 
   test('should render fetching w/ 100 progress query', async () => {
     const { getByRole, getByText } = setup(
-      mockedProps,
-      mockStore(fetchingQueryState),
+      fetchingQueryProps,
+      mockStore(initialState),
     );
     const loading = getByRole('status');
     expect(loading).toBeInTheDocument();
@@ -300,10 +209,7 @@ describe('ResultSet', () => {
 
   test('should render a failed query with an error message', async () => {
     await waitFor(() => {
-      setup(
-        { ...mockedProps, queryId: failedQueryWithErrorMessage.id },
-        mockStore(failedQueryWithErrorMessageState),
-      );
+      setup(failedQueryWithErrorMessageProps, mockStore(initialState));
     });
 
     expect(screen.getByText('Database error')).toBeInTheDocument();
@@ -312,129 +218,44 @@ describe('ResultSet', () => {
 
   test('should render a failed query with an errors object', async () => {
     await waitFor(() => {
-      setup(
-        { ...mockedProps, queryId: failedQueryWithErrors.id },
-        mockStore(failedQueryWithErrorsState),
-      );
+      setup(failedQueryWithErrorsProps, mockStore(initialState));
     });
     expect(screen.getByText('Database error')).toBeInTheDocument();
   });
 
   test('renders if there is no limit in query.results but has queryLimit', async () => {
-    const query = {
-      ...queries[0],
-    };
-    await waitFor(() => {
-      setup(
-        mockedProps,
-        mockStore({
-          ...initialState,
-          user,
-          sqlLab: {
-            ...initialState.sqlLab,
-            queries: {
-              [query.id]: query,
-            },
-          },
-        }),
-      );
-    });
     const { getByRole } = setup(mockedProps, mockStore(initialState));
     expect(getByRole('table')).toBeInTheDocument();
   });
 
   test('renders if there is a limit in query.results but not queryLimit', async () => {
-    const props = { ...mockedProps, queryId: queryWithNoQueryLimit.id };
-    const { getByRole } = setup(
-      props,
-      mockStore({
-        ...initialState,
-        user,
-        sqlLab: {
-          ...initialState.sqlLab,
-          queries: {
-            [queryWithNoQueryLimit.id]: queryWithNoQueryLimit,
-          },
-        },
-      }),
-    );
+    const props = { ...mockedProps, query: queryWithNoQueryLimit };
+    const { getByRole } = setup(props, mockStore(initialState));
     expect(getByRole('table')).toBeInTheDocument();
   });
 
   test('Async queries - renders "Fetch data preview" button when data preview has no results', () => {
-    const asyncRefetchDataPreviewQuery = {
-      ...queries[0],
-      state: 'success',
-      results: undefined,
-      isDataPreview: true,
-    };
-    setup(
-      { ...asyncQueryProps, queryId: asyncRefetchDataPreviewQuery.id },
-      mockStore({
-        ...initialState,
-        user,
-        sqlLab: {
-          ...initialState.sqlLab,
-          queries: {
-            [asyncRefetchDataPreviewQuery.id]: asyncRefetchDataPreviewQuery,
-          },
-        },
-      }),
-    );
+    setup(asyncRefetchDataPreviewProps, mockStore(initialState));
     expect(
       screen.getByRole('button', {
         name: /fetch data preview/i,
       }),
     ).toBeVisible();
-    expect(screen.queryByRole('table')).toBe(null);
+    expect(screen.queryByRole('grid')).toBe(null);
   });
 
   test('Async queries - renders "Refetch results" button when a query has no results', () => {
-    const asyncRefetchResultsTableQuery = {
-      ...queries[0],
-      state: 'success',
-      results: undefined,
-      resultsKey: 'async results key',
-    };
-
-    setup(
-      { ...asyncQueryProps, queryId: asyncRefetchResultsTableQuery.id },
-      mockStore({
-        ...initialState,
-        user,
-        sqlLab: {
-          ...initialState.sqlLab,
-          queries: {
-            [asyncRefetchResultsTableQuery.id]: asyncRefetchResultsTableQuery,
-          },
-        },
-      }),
-    );
+    setup(asyncRefetchResultsTableProps, mockStore(initialState));
     expect(
       screen.getByRole('button', {
         name: /refetch results/i,
       }),
     ).toBeVisible();
-    expect(screen.queryByRole('table')).toBe(null);
+    expect(screen.queryByRole('grid')).toBe(null);
   });
 
   test('Async queries - renders on the first call', () => {
-    const query = {
-      ...queries[0],
-    };
-    setup(
-      { ...asyncQueryProps, queryId: query.id },
-      mockStore({
-        ...initialState,
-        user,
-        sqlLab: {
-          ...initialState.sqlLab,
-          queries: {
-            [query.id]: query,
-          },
-        },
-      }),
-    );
+    setup(asyncQueryProps, mockStore(initialState));
     expect(screen.getByRole('table')).toBeVisible();
     expect(
       screen.queryByRole('button', {
diff --git a/superset-frontend/src/SqlLab/components/ResultSet/index.tsx b/superset-frontend/src/SqlLab/components/ResultSet/index.tsx
index 69e4508434..58e55a1df7 100644
--- a/superset-frontend/src/SqlLab/components/ResultSet/index.tsx
+++ b/superset-frontend/src/SqlLab/components/ResultSet/index.tsx
@@ -17,14 +17,14 @@
  * under the License.
  */
 import React, { useCallback, useEffect, useState } from 'react';
-import { shallowEqual, useDispatch, useSelector } from 'react-redux';
+import { useDispatch } from 'react-redux';
 import { useHistory } from 'react-router-dom';
-import pick from 'lodash/pick';
 import ButtonGroup from 'src/components/ButtonGroup';
 import Alert from 'src/components/Alert';
 import Button from 'src/components/Button';
 import shortid from 'shortid';
 import {
+  QueryResponse,
   QueryState,
   styled,
   t,
@@ -41,7 +41,8 @@ import {
   ISimpleColumn,
   SaveDatasetModal,
 } from 'src/SqlLab/components/SaveDatasetModal';
-import { EXPLORE_CHART_DEFAULT, SqlLabRootState } from 'src/SqlLab/types';
+import { UserWithPermissionsAndRoles } from 'src/types/bootstrapTypes';
+import { EXPLORE_CHART_DEFAULT } from 'src/SqlLab/types';
 import { mountExploreUrl } from 'src/explore/exploreUtils';
 import { postFormData } from 'src/explore/exploreUtils/formData';
 import ProgressBar from 'src/components/ProgressBar';
@@ -81,11 +82,12 @@ export interface ResultSetProps {
   database?: Record<string, any>;
   displayLimit: number;
   height: number;
-  queryId: string;
+  query: QueryResponse;
   search?: boolean;
   showSql?: boolean;
   showSqlInline?: boolean;
   visualize?: boolean;
+  user: UserWithPermissionsAndRoles;
   defaultQueryLimit: number;
 }
 
@@ -143,44 +145,14 @@ const ResultSet = ({
   database = {},
   displayLimit,
   height,
-  queryId,
+  query,
   search = true,
   showSql = false,
   showSqlInline = false,
   visualize = true,
+  user,
   defaultQueryLimit,
 }: ResultSetProps) => {
-  const user = useSelector(({ user }: SqlLabRootState) => user, shallowEqual);
-  const query = useSelector(
-    ({ sqlLab: { queries } }: SqlLabRootState) =>
-      pick(queries[queryId], [
-        'id',
-        'errorMessage',
-        'cached',
-        'results',
-        'resultsKey',
-        'dbId',
-        'tab',
-        'sql',
-        'templateParams',
-        'schema',
-        'rows',
-        'queryLimit',
-        'limitingFactor',
-        'trackingUrl',
-        'state',
-        'errors',
-        'link',
-        'ctas',
-        'ctas_method',
-        'tempSchema',
-        'tempTable',
-        'isDataPreview',
-        'progress',
-        'extra',
-      ]),
-    shallowEqual,
-  );
   const ResultTable =
     extensionsRegistry.get('sqleditor.extension.resultTable') ??
     FilterableTable;
@@ -207,8 +179,8 @@ const ResultSet = ({
     reRunQueryIfSessionTimeoutErrorOnMount();
   }, [reRunQueryIfSessionTimeoutErrorOnMount]);
 
-  const fetchResults = (q: typeof query) => {
-    dispatch(fetchQueryResults(q, displayLimit));
+  const fetchResults = (query: QueryResponse) => {
+    dispatch(fetchQueryResults(query, displayLimit));
   };
 
   const prevQuery = usePrevious(query);
@@ -507,7 +479,7 @@ const ResultSet = ({
       <ResultlessStyles>
         <ErrorMessageWithStackTrace
           title={t('Database error')}
-          error={query?.extra?.errors?.[0] || query?.errors?.[0]}
+          error={query?.errors?.[0]}
           subtitle={<MonospaceDiv>{query.errorMessage}</MonospaceDiv>}
           copyText={query.errorMessage || undefined}
           link={query.link}
@@ -690,4 +662,4 @@ const ResultSet = ({
   );
 };
 
-export default React.memo(ResultSet);
+export default ResultSet;
diff --git a/superset-frontend/src/SqlLab/components/SouthPane/Results.test.tsx b/superset-frontend/src/SqlLab/components/SouthPane/Results.test.tsx
deleted file mode 100644
index c70c039fe5..0000000000
--- a/superset-frontend/src/SqlLab/components/SouthPane/Results.test.tsx
+++ /dev/null
@@ -1,135 +0,0 @@
-/**
- * Licensed to the Apache Software Foundation (ASF) under one
- * or more contributor license agreements.  See the NOTICE file
- * distributed with this work for additional information
- * regarding copyright ownership.  The ASF licenses this file
- * to you under the Apache License, Version 2.0 (the
- * "License"); you may not use this file except in compliance
- * with the License.  You may obtain a copy of the License at
- *
- *   http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing,
- * software distributed under the License is distributed on an
- * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
- * KIND, either express or implied.  See the License for the
- * specific language governing permissions and limitations
- * under the License.
- */
-import React from 'react';
-import { render } from 'spec/helpers/testing-library';
-import { initialState, table, defaultQueryEditor } from 'src/SqlLab/fixtures';
-import { denormalizeTimestamp } from '@superset-ui/core';
-import { LOCALSTORAGE_MAX_QUERY_AGE_MS } from 'src/SqlLab/constants';
-import Results from './Results';
-
-const mockedProps = {
-  queryEditorId: defaultQueryEditor.id,
-  latestQueryId: 'LCly_kkIN',
-  height: 1,
-  displayLimit: 1,
-  defaultQueryLimit: 100,
-};
-
-const mockedEmptyProps = {
-  queryEditorId: 'random_id',
-  latestQueryId: 'empty_query_id',
-  height: 100,
-  displayLimit: 100,
-  defaultQueryLimit: 100,
-};
-
-const mockedExpiredProps = {
-  ...mockedEmptyProps,
-  latestQueryId: 'expired_query_id',
-};
-
-const latestQueryProgressMsg = 'LATEST QUERY MESSAGE - LCly_kkIN';
-const expireDateTime = Date.now() - LOCALSTORAGE_MAX_QUERY_AGE_MS - 1;
-
-const mockState = {
-  ...initialState,
-  sqlLab: {
-    ...initialState,
-    offline: false,
-    tables: [
-      {
-        ...table,
-        dataPreviewQueryId: '2g2_iRFMl',
-        queryEditorId: defaultQueryEditor.id,
-      },
-    ],
-    databases: {},
-    queries: {
-      LCly_kkIN: {
-        cached: false,
-        changed_on: denormalizeTimestamp(new Date().toISOString()),
-        db: 'main',
-        dbId: 1,
-        id: 'LCly_kkIN',
-        startDttm: Date.now(),
-        sqlEditorId: defaultQueryEditor.id,
-        extra: { progress: latestQueryProgressMsg },
-        sql: 'select * from table1',
-      },
-      lXJa7F9_r: {
-        cached: false,
-        changed_on: denormalizeTimestamp(new Date(1559238500401).toISOString()),
-        db: 'main',
-        dbId: 1,
-        id: 'lXJa7F9_r',
-        startDttm: 1559238500401,
-        sqlEditorId: defaultQueryEditor.id,
-        sql: 'select * from table2',
-      },
-      '2g2_iRFMl': {
-        cached: false,
-        changed_on: denormalizeTimestamp(new Date(1559238506925).toISOString()),
-        db: 'main',
-        dbId: 1,
-        id: '2g2_iRFMl',
-        startDttm: 1559238506925,
-        sqlEditorId: defaultQueryEditor.id,
-        sql: 'select * from table3',
-      },
-      expired_query_id: {
-        cached: false,
-        changed_on: denormalizeTimestamp(
-          new Date(expireDateTime).toISOString(),
-        ),
-        db: 'main',
-        dbId: 1,
-        id: 'expired_query_id',
-        startDttm: expireDateTime,
-        sqlEditorId: defaultQueryEditor.id,
-        sql: 'select * from table4',
-      },
-    },
-  },
-};
-
-test('Renders an empty state for results', async () => {
-  const { getByText } = render(<Results {...mockedEmptyProps} />, {
-    useRedux: true,
-    initialState: mockState,
-  });
-  const emptyStateText = getByText(/run a query to display results/i);
-  expect(emptyStateText).toBeVisible();
-});
-
-test('Renders an empty state for expired results', async () => {
-  const { getByText } = render(<Results {...mockedExpiredProps} />, {
-    useRedux: true,
-    initialState: mockState,
-  });
-  const emptyStateText = getByText(/run a query to display results/i);
-  expect(emptyStateText).toBeVisible();
-});
-
-test('should pass latest query down to ResultSet component', async () => {
-  const { getByText } = render(<Results {...mockedProps} />, {
-    useRedux: true,
-    initialState: mockState,
-  });
-  expect(getByText(latestQueryProgressMsg)).toBeVisible();
-});
diff --git a/superset-frontend/src/SqlLab/components/SouthPane/Results.tsx b/superset-frontend/src/SqlLab/components/SouthPane/Results.tsx
deleted file mode 100644
index 4e1b6219ae..0000000000
--- a/superset-frontend/src/SqlLab/components/SouthPane/Results.tsx
+++ /dev/null
@@ -1,106 +0,0 @@
-/**
- * Licensed to the Apache Software Foundation (ASF) under one
- * or more contributor license agreements.  See the NOTICE file
- * distributed with this work for additional information
- * regarding copyright ownership.  The ASF licenses this file
- * to you under the Apache License, Version 2.0 (the
- * "License"); you may not use this file except in compliance
- * with the License.  You may obtain a copy of the License at
- *
- *   http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing,
- * software distributed under the License is distributed on an
- * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
- * KIND, either express or implied.  See the License for the
- * specific language governing permissions and limitations
- * under the License.
- */
-import React from 'react';
-import { shallowEqual, useSelector } from 'react-redux';
-import Alert from 'src/components/Alert';
-import { EmptyStateMedium } from 'src/components/EmptyState';
-import { FeatureFlag, styled, t, isFeatureEnabled } from '@superset-ui/core';
-
-import { SqlLabRootState } from 'src/SqlLab/types';
-import ResultSet from '../ResultSet';
-import { LOCALSTORAGE_MAX_QUERY_AGE_MS } from '../../constants';
-
-const EXTRA_HEIGHT_RESULTS = 8; // we need extra height in RESULTS tab. because the height from props was calculated based on PREVIEW tab.
-
-type Props = {
-  latestQueryId: string;
-  height: number;
-  displayLimit: number;
-  defaultQueryLimit: number;
-};
-
-const StyledEmptyStateWrapper = styled.div`
-  height: 100%;
-  .ant-empty-image img {
-    margin-right: 28px;
-  }
-
-  p {
-    margin-right: 28px;
-  }
-`;
-
-const Results: React.FC<Props> = ({
-  latestQueryId,
-  height,
-  displayLimit,
-  defaultQueryLimit,
-}) => {
-  const databases = useSelector(
-    ({ sqlLab: { databases } }: SqlLabRootState) => databases,
-    shallowEqual,
-  );
-  const latestQuery = useSelector(
-    ({ sqlLab: { queries } }: SqlLabRootState) => queries[latestQueryId || ''],
-    shallowEqual,
-  );
-
-  if (
-    !latestQuery ||
-    Date.now() - latestQuery.startDttm > LOCALSTORAGE_MAX_QUERY_AGE_MS
-  ) {
-    return (
-      <StyledEmptyStateWrapper>
-        <EmptyStateMedium
-          title={t('Run a query to display results')}
-          image="document.svg"
-        />
-      </StyledEmptyStateWrapper>
-    );
-  }
-
-  if (
-    isFeatureEnabled(FeatureFlag.SQLLAB_BACKEND_PERSISTENCE) &&
-    latestQuery.state === 'success' &&
-    !latestQuery.resultsKey &&
-    !latestQuery.results
-  ) {
-    return (
-      <Alert
-        type="warning"
-        message={t('No stored results found, you need to re-run your query')}
-      />
-    );
-  }
-
-  return (
-    <ResultSet
-      search
-      queryId={latestQuery.id}
-      height={height + EXTRA_HEIGHT_RESULTS}
-      database={databases[latestQuery.dbId]}
-      displayLimit={displayLimit}
-      defaultQueryLimit={defaultQueryLimit}
-      showSql
-      showSqlInline
-    />
-  );
-};
-
-export default Results;
diff --git a/superset-frontend/src/SqlLab/components/SouthPane/SouthPane.test.tsx b/superset-frontend/src/SqlLab/components/SouthPane/SouthPane.test.tsx
index c978a4ca32..80a102ff21 100644
--- a/superset-frontend/src/SqlLab/components/SouthPane/SouthPane.test.tsx
+++ b/superset-frontend/src/SqlLab/components/SouthPane/SouthPane.test.tsx
@@ -17,12 +17,15 @@
  * under the License.
  */
 import React from 'react';
-import { render } from 'spec/helpers/testing-library';
-import SouthPane from 'src/SqlLab/components/SouthPane';
+import configureStore from 'redux-mock-store';
+import thunk from 'redux-thunk';
+import { render, screen, waitFor } from 'spec/helpers/testing-library';
+import SouthPane, { SouthPaneProps } from 'src/SqlLab/components/SouthPane';
 import '@testing-library/jest-dom/extend-expect';
 import { STATUS_OPTIONS } from 'src/SqlLab/constants';
 import { initialState, table, defaultQueryEditor } from 'src/SqlLab/fixtures';
 import { denormalizeTimestamp } from '@superset-ui/core';
+import { Store } from 'redux';
 
 const mockedProps = {
   queryEditorId: defaultQueryEditor.id,
@@ -34,32 +37,29 @@ const mockedProps = {
 
 const mockedEmptyProps = {
   queryEditorId: 'random_id',
-  latestQueryId: 'empty_query_id',
+  latestQueryId: '',
   height: 100,
   displayLimit: 100,
   defaultQueryLimit: 100,
 };
 
+jest.mock('src/SqlLab/components/SqlEditorLeftBar', () => jest.fn());
+
 const latestQueryProgressMsg = 'LATEST QUERY MESSAGE - LCly_kkIN';
 
-const mockState = {
+const middlewares = [thunk];
+const mockStore = configureStore(middlewares);
+const store = mockStore({
   ...initialState,
   sqlLab: {
-    ...initialState.sqlLab,
+    ...initialState,
     offline: false,
     tables: [
       {
         ...table,
-        name: 'table3',
         dataPreviewQueryId: '2g2_iRFMl',
         queryEditorId: defaultQueryEditor.id,
       },
-      {
-        ...table,
-        name: 'table4',
-        dataPreviewQueryId: 'erWdqEWPm',
-        queryEditorId: defaultQueryEditor.id,
-      },
     ],
     databases: {},
     queries: {
@@ -72,7 +72,6 @@ const mockState = {
         startDttm: Date.now(),
         sqlEditorId: defaultQueryEditor.id,
         extra: { progress: latestQueryProgressMsg },
-        sql: 'select * from table1',
       },
       lXJa7F9_r: {
         cached: false,
@@ -82,7 +81,6 @@ const mockState = {
         id: 'lXJa7F9_r',
         startDttm: 1559238500401,
         sqlEditorId: defaultQueryEditor.id,
-        sql: 'select * from table2',
       },
       '2g2_iRFMl': {
         cached: false,
@@ -92,7 +90,6 @@ const mockState = {
         id: '2g2_iRFMl',
         startDttm: 1559238506925,
         sqlEditorId: defaultQueryEditor.id,
-        sql: 'select * from table3',
       },
       erWdqEWPm: {
         cached: false,
@@ -102,38 +99,44 @@ const mockState = {
         id: 'erWdqEWPm',
         startDttm: 1559238516395,
         sqlEditorId: defaultQueryEditor.id,
-        sql: 'select * from table4',
       },
     },
   },
-};
-
-test('should render offline when the state is offline', async () => {
-  const { getByText } = render(<SouthPane {...mockedEmptyProps} />, {
+});
+const setup = (props: SouthPaneProps, store: Store) =>
+  render(<SouthPane {...props} />, {
     useRedux: true,
-    initialState: {
-      ...initialState,
-      sqlLab: {
-        ...initialState.sqlLab,
-        offline: true,
-      },
-    },
+    ...(store && { store }),
   });
 
-  expect(getByText(STATUS_OPTIONS.offline)).toBeVisible();
-});
+describe('SouthPane', () => {
+  const renderAndWait = (props: SouthPaneProps, store: Store) =>
+    waitFor(async () => setup(props, store));
 
-test('should render tabs for table preview queries', () => {
-  const { getAllByRole } = render(<SouthPane {...mockedProps} />, {
-    useRedux: true,
-    initialState: mockState,
+  it('Renders an empty state for results', async () => {
+    await renderAndWait(mockedEmptyProps, store);
+    const emptyStateText = screen.getByText(/run a query to display results/i);
+    expect(emptyStateText).toBeVisible();
   });
 
-  const tabs = getAllByRole('tab');
-  expect(tabs).toHaveLength(mockState.sqlLab.tables.length + 2);
-  expect(tabs[0]).toHaveTextContent('Results');
-  expect(tabs[1]).toHaveTextContent('Query history');
-  mockState.sqlLab.tables.forEach(({ name }, index) => {
-    expect(tabs[index + 2]).toHaveTextContent(`Preview: \`${name}\``);
+  it('should render offline when the state is offline', async () => {
+    await renderAndWait(
+      mockedEmptyProps,
+      mockStore({
+        ...initialState,
+        sqlLab: {
+          ...initialState.sqlLab,
+          offline: true,
+        },
+      }),
+    );
+
+    expect(screen.getByText(STATUS_OPTIONS.offline)).toBeVisible();
+  });
+
+  it('should pass latest query down to ResultSet component', async () => {
+    await renderAndWait(mockedProps, store);
+
+    expect(screen.getByText(latestQueryProgressMsg)).toBeVisible();
   });
 });
diff --git a/superset-frontend/src/SqlLab/components/SouthPane/index.tsx b/superset-frontend/src/SqlLab/components/SouthPane/index.tsx
index 0bbce99b1c..38a20f9f6d 100644
--- a/superset-frontend/src/SqlLab/components/SouthPane/index.tsx
+++ b/superset-frontend/src/SqlLab/components/SouthPane/index.tsx
@@ -19,8 +19,10 @@
 import React, { createRef, useMemo } from 'react';
 import { shallowEqual, useDispatch, useSelector } from 'react-redux';
 import shortid from 'shortid';
+import Alert from 'src/components/Alert';
 import Tabs from 'src/components/Tabs';
-import { styled, t } from '@superset-ui/core';
+import { EmptyStateMedium } from 'src/components/EmptyState';
+import { FeatureFlag, styled, t, isFeatureEnabled } from '@superset-ui/core';
 
 import { setActiveSouthPaneTab } from 'src/SqlLab/actions/sqlLab';
 
@@ -31,11 +33,11 @@ import ResultSet from '../ResultSet';
 import {
   STATUS_OPTIONS,
   STATE_TYPE_MAP,
+  LOCALSTORAGE_MAX_QUERY_AGE_MS,
   STATUS_OPTIONS_LOCALIZED,
 } from '../../constants';
-import Results from './Results';
 
-const TAB_HEIGHT = 130;
+const TAB_HEIGHT = 140;
 
 /*
     editorQueries are queries executed by users passed from SqlEditor component
@@ -83,6 +85,18 @@ const StyledPane = styled.div<StyledPaneProps>`
   }
 `;
 
+const EXTRA_HEIGHT_RESULTS = 24; // we need extra height in RESULTS tab. because the height from props was calculated based on PREVIEW tab.
+const StyledEmptyStateWrapper = styled.div`
+  height: 100%;
+  .ant-empty-image img {
+    margin-right: 28px;
+  }
+
+  p {
+    margin-right: 28px;
+  }
+`;
+
 const SouthPane = ({
   queryEditorId,
   latestQueryId,
@@ -91,43 +105,128 @@ const SouthPane = ({
   defaultQueryLimit,
 }: SouthPaneProps) => {
   const dispatch = useDispatch();
-  const { offline, tables } = useSelector(
-    ({ sqlLab: { offline, tables } }: SqlLabRootState) => ({
+  const user = useSelector(({ user }: SqlLabRootState) => user, shallowEqual);
+  const { databases, offline, queries, tables } = useSelector(
+    ({ sqlLab: { databases, offline, queries, tables } }: SqlLabRootState) => ({
+      databases,
       offline,
+      queries,
       tables,
     }),
     shallowEqual,
   );
-  const queries = useSelector(
-    ({ sqlLab: { queries } }: SqlLabRootState) => Object.keys(queries),
-    shallowEqual,
+  const editorQueries = useMemo(
+    () =>
+      Object.values(queries).filter(
+        ({ sqlEditorId }) => sqlEditorId === queryEditorId,
+      ),
+    [queries, queryEditorId],
   );
+  const dataPreviewQueries = useMemo(
+    () =>
+      tables
+        .filter(
+          ({ dataPreviewQueryId, queryEditorId: qeId }) =>
+            dataPreviewQueryId &&
+            queryEditorId === qeId &&
+            queries[dataPreviewQueryId],
+        )
+        .map(({ name, dataPreviewQueryId }) => ({
+          ...queries[dataPreviewQueryId || ''],
+          tableName: name,
+        })),
+    [queries, queryEditorId, tables],
+  );
+  const latestQuery = useMemo(
+    () => editorQueries.find(({ id }) => id === latestQueryId),
+    [editorQueries, latestQueryId],
+  );
+
   const activeSouthPaneTab =
     useSelector<SqlLabRootState, string>(
       state => state.sqlLab.activeSouthPaneTab as string,
     ) ?? 'Results';
-
-  const querySet = useMemo(() => new Set(queries), [queries]);
-  const dataPreviewQueries = useMemo(
-    () =>
-      tables.filter(
-        ({ dataPreviewQueryId, queryEditorId: qeId }) =>
-          dataPreviewQueryId &&
-          queryEditorId === qeId &&
-          querySet.has(dataPreviewQueryId),
-      ),
-    [queryEditorId, tables, querySet],
-  );
   const innerTabContentHeight = height - TAB_HEIGHT;
   const southPaneRef = createRef<HTMLDivElement>();
   const switchTab = (id: string) => {
     dispatch(setActiveSouthPaneTab(id));
   };
-
-  return offline ? (
+  const renderOfflineStatus = () => (
     <Label className="m-r-3" type={STATE_TYPE_MAP[STATUS_OPTIONS.offline]}>
       {STATUS_OPTIONS_LOCALIZED.offline}
     </Label>
+  );
+
+  const renderResults = () => {
+    let results;
+    if (latestQuery) {
+      if (latestQuery?.extra?.errors) {
+        latestQuery.errors = latestQuery.extra.errors;
+      }
+      if (
+        isFeatureEnabled(FeatureFlag.SQLLAB_BACKEND_PERSISTENCE) &&
+        latestQuery.state === 'success' &&
+        !latestQuery.resultsKey &&
+        !latestQuery.results
+      ) {
+        results = (
+          <Alert
+            type="warning"
+            message={t(
+              'No stored results found, you need to re-run your query',
+            )}
+          />
+        );
+        return results;
+      }
+      if (Date.now() - latestQuery.startDttm <= LOCALSTORAGE_MAX_QUERY_AGE_MS) {
+        results = (
+          <ResultSet
+            search
+            query={latestQuery}
+            user={user}
+            height={innerTabContentHeight + EXTRA_HEIGHT_RESULTS}
+            database={databases[latestQuery.dbId]}
+            displayLimit={displayLimit}
+            defaultQueryLimit={defaultQueryLimit}
+            showSql
+            showSqlInline
+          />
+        );
+      }
+    } else {
+      results = (
+        <StyledEmptyStateWrapper>
+          <EmptyStateMedium
+            title={t('Run a query to display results')}
+            image="document.svg"
+          />
+        </StyledEmptyStateWrapper>
+      );
+    }
+    return results;
+  };
+
+  const renderDataPreviewTabs = () =>
+    dataPreviewQueries.map(query => (
+      <Tabs.TabPane
+        tab={t('Preview: `%s`', decodeURIComponent(query.tableName))}
+        key={query.id}
+      >
+        <ResultSet
+          query={query}
+          visualize={false}
+          csv={false}
+          cache
+          user={user}
+          height={innerTabContentHeight}
+          displayLimit={displayLimit}
+          defaultQueryLimit={defaultQueryLimit}
+        />
+      </Tabs.TabPane>
+    ));
+  return offline ? (
+    renderOfflineStatus()
   ) : (
     <StyledPane
       data-test="south-pane"
@@ -144,41 +243,16 @@ const SouthPane = ({
         animated={false}
       >
         <Tabs.TabPane tab={t('Results')} key="Results">
-          {latestQueryId && (
-            <Results
-              height={innerTabContentHeight}
-              latestQueryId={latestQueryId}
-              displayLimit={displayLimit}
-              defaultQueryLimit={defaultQueryLimit}
-            />
-          )}
+          {renderResults()}
         </Tabs.TabPane>
         <Tabs.TabPane tab={t('Query history')} key="History">
           <QueryHistory
-            queryEditorId={queryEditorId}
+            queries={editorQueries}
             displayLimit={displayLimit}
             latestQueryId={latestQueryId}
           />
         </Tabs.TabPane>
-        {dataPreviewQueries.map(
-          ({ name, dataPreviewQueryId }) =>
-            dataPreviewQueryId && (
-              <Tabs.TabPane
-                tab={t('Preview: `%s`', decodeURIComponent(name))}
-                key={dataPreviewQueryId}
-              >
-                <ResultSet
-                  queryId={dataPreviewQueryId}
-                  visualize={false}
-                  csv={false}
-                  cache
-                  height={innerTabContentHeight}
-                  displayLimit={displayLimit}
-                  defaultQueryLimit={defaultQueryLimit}
-                />
-              </Tabs.TabPane>
-            ),
-        )}
+        {renderDataPreviewTabs()}
       </Tabs>
     </StyledPane>
   );
diff --git a/superset-frontend/src/SqlLab/components/SqlEditor/SqlEditor.test.tsx b/superset-frontend/src/SqlLab/components/SqlEditor/SqlEditor.test.tsx
index 6a25492ce5..63f67170d0 100644
--- a/superset-frontend/src/SqlLab/components/SqlEditor/SqlEditor.test.tsx
+++ b/superset-frontend/src/SqlLab/components/SqlEditor/SqlEditor.test.tsx
@@ -145,8 +145,8 @@ describe('SqlEditor', () => {
     (SqlEditorLeftBar as jest.Mock).mockImplementation(() => (
       <div data-test="mock-sql-editor-left-bar" />
     ));
-    (ResultSet as unknown as jest.Mock).mockClear();
-    (ResultSet as unknown as jest.Mock).mockImplementation(() => (
+    (ResultSet as jest.Mock).mockClear();
+    (ResultSet as jest.Mock).mockImplementation(() => (
       <div data-test="mock-result-set" />
     ));
   });
@@ -182,8 +182,7 @@ describe('SqlEditor', () => {
     const editor = await findByTestId('react-ace');
     const sql = 'select *';
     const renderCount = (SqlEditorLeftBar as jest.Mock).mock.calls.length;
-    const renderCountForSouthPane = (ResultSet as unknown as jest.Mock).mock
-      .calls.length;
+    const renderCountForSouthPane = (ResultSet as jest.Mock).mock.calls.length;
     expect(SqlEditorLeftBar).toHaveBeenCalledTimes(renderCount);
     expect(ResultSet).toHaveBeenCalledTimes(renderCountForSouthPane);
     fireEvent.change(editor, { target: { value: sql } });
diff --git a/superset-frontend/src/SqlLab/reducers/sqlLab.js b/superset-frontend/src/SqlLab/reducers/sqlLab.js
index ce9eed9b9d..59bd0558a1 100644
--- a/superset-frontend/src/SqlLab/reducers/sqlLab.js
+++ b/superset-frontend/src/SqlLab/reducers/sqlLab.js
@@ -345,7 +345,7 @@ export default function sqlLabReducer(state = {}, action) {
         return state;
       }
       const alts = {
-        endDttm: action?.results?.query?.endDttm || now(),
+        endDttm: now(),
         progress: 100,
         results: action.results,
         rows: action?.results?.query?.rows || 0,
@@ -674,14 +674,7 @@ export default function sqlLabReducer(state = {}, action) {
       if (!change) {
         newQueries = state.queries;
       }
-      return {
-        ...state,
-        queries: newQueries,
-        queriesLastUpdate:
-          queriesLastUpdate > state.queriesLastUpdate
-            ? queriesLastUpdate
-            : Date.now(),
-      };
+      return { ...state, queries: newQueries, queriesLastUpdate };
     },
     [actions.CLEAR_INACTIVE_QUERIES]() {
       const { queries } = state;
@@ -708,11 +701,7 @@ export default function sqlLabReducer(state = {}, action) {
             },
           ]),
       );
-      return {
-        ...state,
-        queries: cleanedQueries,
-        queriesLastUpdate: Date.now(),
-      };
+      return { ...state, queries: cleanedQueries };
     },
     [actions.SET_USER_OFFLINE]() {
       return { ...state, offline: action.offline };
diff --git a/superset-frontend/src/SqlLab/reducers/sqlLab.test.js b/superset-frontend/src/SqlLab/reducers/sqlLab.test.js
index 5a70f10bb3..e1a234734b 100644
--- a/superset-frontend/src/SqlLab/reducers/sqlLab.test.js
+++ b/superset-frontend/src/SqlLab/reducers/sqlLab.test.js
@@ -20,7 +20,6 @@ import { QueryState } from '@superset-ui/core';
 import sqlLabReducer from 'src/SqlLab/reducers/sqlLab';
 import * as actions from 'src/SqlLab/actions/sqlLab';
 import { table, initialState as mockState } from '../fixtures';
-import { QUERY_UPDATE_FREQ } from '../components/QueryAutoRefresh';
 
 const initialState = mockState.sqlLab;
 
@@ -405,7 +404,6 @@ describe('sqlLabReducer', () => {
       };
     });
     it('updates queries that have already been completed', () => {
-      const current = Date.now();
       newState = sqlLabReducer(
         {
           ...newState,
@@ -420,10 +418,9 @@ describe('sqlLabReducer', () => {
             },
           },
         },
-        actions.clearInactiveQueries(QUERY_UPDATE_FREQ),
+        actions.clearInactiveQueries(Date.now()),
       );
       expect(newState.queries.abcd.state).toBe(QueryState.SUCCESS);
-      expect(newState.queriesLastUpdate).toBeGreaterThanOrEqual(current);
     });
   });
 });


(superset) 01/06: fix(plugin-chart-echarts): undefined bounds for bubble chart (#26243)

Posted by mi...@apache.org.
This is an automated email from the ASF dual-hosted git repository.

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

commit 0ac833d0c57dc918c1ddbc57f776775ee1a3cf84
Author: Ville Brofeldt <33...@users.noreply.github.com>
AuthorDate: Tue Dec 12 09:22:29 2023 -0800

    fix(plugin-chart-echarts): undefined bounds for bubble chart (#26243)
    
    (cherry picked from commit 5df544b6fb079e98d4ab6839cfbdf7f08358a950)
---
 .../packages/superset-ui-core/src/chart/index.ts   |  2 +-
 .../plugin-chart-echarts/src/Bubble/constants.ts   |  1 +
 .../src/Bubble/transformProps.ts                   |  4 +-
 .../test/Bubble/transformProps.test.ts             | 48 ++++++++++++++++++++--
 4 files changed, 48 insertions(+), 7 deletions(-)

diff --git a/superset-frontend/packages/superset-ui-core/src/chart/index.ts b/superset-frontend/packages/superset-ui-core/src/chart/index.ts
index c1588023a3..bc4b5a20bf 100644
--- a/superset-frontend/packages/superset-ui-core/src/chart/index.ts
+++ b/superset-frontend/packages/superset-ui-core/src/chart/index.ts
@@ -20,7 +20,7 @@
 export { default as ChartClient } from './clients/ChartClient';
 export { default as ChartMetadata } from './models/ChartMetadata';
 export { default as ChartPlugin } from './models/ChartPlugin';
-export { default as ChartProps } from './models/ChartProps';
+export { default as ChartProps, ChartPropsConfig } from './models/ChartProps';
 
 export { default as createLoadableRenderer } from './components/createLoadableRenderer';
 export { default as reactify } from './components/reactify';
diff --git a/superset-frontend/plugins/plugin-chart-echarts/src/Bubble/constants.ts b/superset-frontend/plugins/plugin-chart-echarts/src/Bubble/constants.ts
index 89b03d5e90..1c70e872e6 100644
--- a/superset-frontend/plugins/plugin-chart-echarts/src/Bubble/constants.ts
+++ b/superset-frontend/plugins/plugin-chart-echarts/src/Bubble/constants.ts
@@ -29,6 +29,7 @@ export const DEFAULT_FORM_DATA: Partial<EchartsBubbleFormData> = {
   yAxisTitleMargin: 30,
   truncateXAxis: false,
   truncateYAxis: false,
+  xAxisBounds: [null, null],
   yAxisBounds: [null, null],
   xAxisLabelRotation: defaultXAxis.xAxisLabelRotation,
   opacity: 0.6,
diff --git a/superset-frontend/plugins/plugin-chart-echarts/src/Bubble/transformProps.ts b/superset-frontend/plugins/plugin-chart-echarts/src/Bubble/transformProps.ts
index 01d9ed3c53..754b26003b 100644
--- a/superset-frontend/plugins/plugin-chart-echarts/src/Bubble/transformProps.ts
+++ b/superset-frontend/plugins/plugin-chart-echarts/src/Bubble/transformProps.ts
@@ -143,8 +143,8 @@ export default function transformProps(chartProps: EchartsBubbleChartProps) {
   const yAxisFormatter = getNumberFormatter(yAxisFormat);
   const tooltipSizeFormatter = getNumberFormatter(tooltipSizeFormat);
 
-  const [xAxisMin, xAxisMax] = xAxisBounds.map(parseAxisBound);
-  const [yAxisMin, yAxisMax] = yAxisBounds.map(parseAxisBound);
+  const [xAxisMin, xAxisMax] = (xAxisBounds || []).map(parseAxisBound);
+  const [yAxisMin, yAxisMax] = (yAxisBounds || []).map(parseAxisBound);
 
   const padding = getPadding(
     showLegend,
diff --git a/superset-frontend/plugins/plugin-chart-echarts/test/Bubble/transformProps.test.ts b/superset-frontend/plugins/plugin-chart-echarts/test/Bubble/transformProps.test.ts
index 1a92a43257..d93f394681 100644
--- a/superset-frontend/plugins/plugin-chart-echarts/test/Bubble/transformProps.test.ts
+++ b/superset-frontend/plugins/plugin-chart-echarts/test/Bubble/transformProps.test.ts
@@ -18,6 +18,7 @@
  */
 import {
   ChartProps,
+  ChartPropsConfig,
   getNumberFormatter,
   SqlaFormData,
   supersetTheme,
@@ -27,7 +28,7 @@ import { EchartsBubbleChartProps } from 'plugins/plugin-chart-echarts/src/Bubble
 import transformProps, { formatTooltip } from '../../src/Bubble/transformProps';
 
 describe('Bubble transformProps', () => {
-  const formData: SqlaFormData = {
+  const defaultFormData: SqlaFormData = {
     datasource: '1__table',
     viz_type: 'echarts_bubble',
     entity: 'customer_name',
@@ -51,8 +52,8 @@ describe('Bubble transformProps', () => {
     xAxisBounds: [null, null],
     yAxisBounds: [null, null],
   };
-  const chartProps = new ChartProps({
-    formData,
+  const chartConfig: ChartPropsConfig = {
+    formData: defaultFormData,
     height: 800,
     width: 800,
     queriesData: [
@@ -80,9 +81,48 @@ describe('Bubble transformProps', () => {
       },
     ],
     theme: supersetTheme,
-  });
+  };
 
   it('Should transform props for viz', () => {
+    const chartProps = new ChartProps(chartConfig);
+    expect(transformProps(chartProps as EchartsBubbleChartProps)).toEqual(
+      expect.objectContaining({
+        width: 800,
+        height: 800,
+        echartOptions: expect.objectContaining({
+          series: expect.arrayContaining([
+            expect.objectContaining({
+              data: expect.arrayContaining([
+                [10, 20, 30, 'AV Stores, Co.', null],
+              ]),
+            }),
+            expect.objectContaining({
+              data: expect.arrayContaining([
+                [40, 50, 60, 'Alpha Cognac', null],
+              ]),
+            }),
+            expect.objectContaining({
+              data: expect.arrayContaining([
+                [70, 80, 90, 'Amica Models & Co.', null],
+              ]),
+            }),
+          ]),
+        }),
+      }),
+    );
+  });
+
+  it('Should transform props with undefined control values', () => {
+    const formData: SqlaFormData = {
+      ...defaultFormData,
+      xAxisBounds: undefined,
+      yAxisBounds: undefined,
+    };
+    const chartProps = new ChartProps({
+      ...chartConfig,
+      formData,
+    });
+
     expect(transformProps(chartProps as EchartsBubbleChartProps)).toEqual(
       expect.objectContaining({
         width: 800,


(superset) 04/06: fix(plugin-chart-echarts): use scale for truncating x-axis (#26269)

Posted by mi...@apache.org.
This is an automated email from the ASF dual-hosted git repository.

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

commit 4d0404119c8c7d9b0a0faf929ab939cdf690560e
Author: Ville Brofeldt <33...@users.noreply.github.com>
AuthorDate: Thu Dec 14 10:13:39 2023 -0800

    fix(plugin-chart-echarts): use scale for truncating x-axis (#26269)
    
    (cherry picked from commit 67468c46c0c8c8a03833dd64eb84284890b7091c)
---
 .../src/MixedTimeseries/controlPanel.tsx           |   4 +
 .../src/MixedTimeseries/transformProps.ts          |  52 +++++++----
 .../src/MixedTimeseries/types.ts                   |   1 +
 .../src/Timeseries/transformProps.ts               |   8 +-
 .../plugin-chart-echarts/src/utils/controls.ts     |   1 -
 .../plugin-chart-echarts/src/utils/series.ts       |  40 ++++++--
 .../plugin-chart-echarts/test/utils/series.test.ts | 101 +++++++++++++++++++--
 7 files changed, 170 insertions(+), 37 deletions(-)

diff --git a/superset-frontend/plugins/plugin-chart-echarts/src/MixedTimeseries/controlPanel.tsx b/superset-frontend/plugins/plugin-chart-echarts/src/MixedTimeseries/controlPanel.tsx
index f54b3d01dc..c5fed29847 100644
--- a/superset-frontend/plugins/plugin-chart-echarts/src/MixedTimeseries/controlPanel.tsx
+++ b/superset-frontend/plugins/plugin-chart-echarts/src/MixedTimeseries/controlPanel.tsx
@@ -35,6 +35,8 @@ import { EchartsTimeseriesSeriesType } from '../Timeseries/types';
 import {
   legendSection,
   richTooltipSection,
+  truncateXAxis,
+  xAxisBounds,
   xAxisLabelRotation,
 } from '../controls';
 
@@ -333,6 +335,8 @@ const config: ControlPanelConfig = {
             },
           },
         ],
+        [truncateXAxis],
+        [xAxisBounds],
         [
           {
             name: 'truncateYAxis',
diff --git a/superset-frontend/plugins/plugin-chart-echarts/src/MixedTimeseries/transformProps.ts b/superset-frontend/plugins/plugin-chart-echarts/src/MixedTimeseries/transformProps.ts
index f924ad6f9b..223083ef0c 100644
--- a/superset-frontend/plugins/plugin-chart-echarts/src/MixedTimeseries/transformProps.ts
+++ b/superset-frontend/plugins/plugin-chart-echarts/src/MixedTimeseries/transformProps.ts
@@ -20,32 +20,32 @@
 import { invert } from 'lodash';
 import {
   AnnotationLayer,
+  buildCustomFormatters,
   CategoricalColorNamespace,
+  CurrencyFormatter,
+  ensureIsArray,
   GenericDataType,
+  getCustomFormatter,
   getNumberFormatter,
+  getXAxisLabel,
+  isDefined,
   isEventAnnotationLayer,
   isFormulaAnnotationLayer,
   isIntervalAnnotationLayer,
+  isPhysicalColumn,
   isTimeseriesAnnotationLayer,
   QueryFormData,
+  QueryFormMetric,
   TimeseriesChartDataResponseResult,
   TimeseriesDataRecord,
-  getXAxisLabel,
-  isPhysicalColumn,
-  isDefined,
-  ensureIsArray,
-  buildCustomFormatters,
   ValueFormatter,
-  QueryFormMetric,
-  getCustomFormatter,
-  CurrencyFormatter,
 } from '@superset-ui/core';
 import { getOriginalSeries } from '@superset-ui/chart-controls';
 import { EChartsCoreOption, SeriesOption } from 'echarts';
 import {
   DEFAULT_FORM_DATA,
-  EchartsMixedTimeseriesFormData,
   EchartsMixedTimeseriesChartTransformedProps,
+  EchartsMixedTimeseriesFormData,
   EchartsMixedTimeseriesProps,
 } from './types';
 import {
@@ -55,14 +55,15 @@ import {
 } from '../types';
 import { parseAxisBound } from '../utils/controls';
 import {
-  getOverMaxHiddenFormatter,
   dedupSeries,
+  extractDataTotalValues,
   extractSeries,
+  extractShowValueIndexes,
   getAxisType,
   getColtypesMapping,
   getLegendProps,
-  extractDataTotalValues,
-  extractShowValueIndexes,
+  getMinAndMaxFromBounds,
+  getOverMaxHiddenFormatter,
 } from '../utils/series';
 import {
   extractAnnotationLabels,
@@ -84,7 +85,7 @@ import {
   transformSeries,
   transformTimeseriesAnnotation,
 } from '../Timeseries/transformers';
-import { TIMESERIES_CONSTANTS, TIMEGRAIN_TO_TIMESTAMP } from '../constants';
+import { TIMEGRAIN_TO_TIMESTAMP, TIMESERIES_CONSTANTS } from '../constants';
 import { getDefaultTooltip } from '../utils/tooltip';
 import {
   getTooltipTimeFormatter,
@@ -166,6 +167,7 @@ export default function transformProps(
     showValueB,
     stack,
     stackB,
+    truncateXAxis,
     truncateYAxis,
     tooltipTimeFormat,
     yAxisFormat,
@@ -181,6 +183,7 @@ export default function transformProps(
     zoomable,
     richTooltip,
     tooltipSortByMetric,
+    xAxisBounds,
     xAxisLabelRotation,
     groupby,
     groupbyB,
@@ -345,7 +348,8 @@ export default function transformProps(
     });
 
   // yAxisBounds need to be parsed to replace incompatible values with undefined
-  let [min, max] = (yAxisBounds || []).map(parseAxisBound);
+  const [xAxisMin, xAxisMax] = (xAxisBounds || []).map(parseAxisBound);
+  let [yAxisMin, yAxisMax] = (yAxisBounds || []).map(parseAxisBound);
   let [minSecondary, maxSecondary] = (yAxisBoundsSecondary || []).map(
     parseAxisBound,
   );
@@ -386,7 +390,7 @@ export default function transformProps(
         formatter:
           seriesType === EchartsTimeseriesSeriesType.Bar
             ? getOverMaxHiddenFormatter({
-                max,
+                max: yAxisMax,
                 formatter: seriesFormatter,
               })
             : seriesFormatter,
@@ -447,8 +451,8 @@ export default function transformProps(
 
   // default to 0-100% range when doing row-level contribution chart
   if (contributionMode === 'row' && stack) {
-    if (min === undefined) min = 0;
-    if (max === undefined) max = 1;
+    if (yAxisMin === undefined) yAxisMin = 0;
+    if (yAxisMax === undefined) yAxisMax = 1;
     if (minSecondary === undefined) minSecondary = 0;
     if (maxSecondary === undefined) maxSecondary = 1;
   }
@@ -499,13 +503,23 @@ export default function transformProps(
         xAxisType === 'time' && timeGrainSqla
           ? TIMEGRAIN_TO_TIMESTAMP[timeGrainSqla]
           : 0,
+      ...getMinAndMaxFromBounds(
+        xAxisType,
+        truncateXAxis,
+        xAxisMin,
+        xAxisMax,
+        seriesType === EchartsTimeseriesSeriesType.Bar ||
+          seriesTypeB === EchartsTimeseriesSeriesType.Bar
+          ? EchartsTimeseriesSeriesType.Bar
+          : undefined,
+      ),
     },
     yAxis: [
       {
         ...defaultYAxis,
         type: logAxis ? 'log' : 'value',
-        min,
-        max,
+        min: yAxisMin,
+        max: yAxisMax,
         minorTick: { show: true },
         minorSplitLine: { show: minorSplitLine },
         axisLabel: {
diff --git a/superset-frontend/plugins/plugin-chart-echarts/src/MixedTimeseries/types.ts b/superset-frontend/plugins/plugin-chart-echarts/src/MixedTimeseries/types.ts
index 30969ae367..2e9ba641aa 100644
--- a/superset-frontend/plugins/plugin-chart-echarts/src/MixedTimeseries/types.ts
+++ b/superset-frontend/plugins/plugin-chart-echarts/src/MixedTimeseries/types.ts
@@ -104,6 +104,7 @@ export const DEFAULT_FORM_DATA: EchartsMixedTimeseriesFormData = {
   yAxisFormatSecondary: TIMESERIES_DEFAULTS.yAxisFormat,
   yAxisTitleSecondary: DEFAULT_TITLE_FORM_DATA.yAxisTitle,
   tooltipTimeFormat: TIMESERIES_DEFAULTS.tooltipTimeFormat,
+  xAxisBounds: TIMESERIES_DEFAULTS.xAxisBounds,
   xAxisTimeFormat: TIMESERIES_DEFAULTS.xAxisTimeFormat,
   area: TIMESERIES_DEFAULTS.area,
   areaB: TIMESERIES_DEFAULTS.area,
diff --git a/superset-frontend/plugins/plugin-chart-echarts/src/Timeseries/transformProps.ts b/superset-frontend/plugins/plugin-chart-echarts/src/Timeseries/transformProps.ts
index 8dd9966484..0d54fd114c 100644
--- a/superset-frontend/plugins/plugin-chart-echarts/src/Timeseries/transformProps.ts
+++ b/superset-frontend/plugins/plugin-chart-echarts/src/Timeseries/transformProps.ts
@@ -460,7 +460,13 @@ export default function transformProps(
       xAxisType === AxisType.time && timeGrainSqla
         ? TIMEGRAIN_TO_TIMESTAMP[timeGrainSqla]
         : 0,
-    ...getMinAndMaxFromBounds(xAxisType, truncateXAxis, xAxisMin, xAxisMax),
+    ...getMinAndMaxFromBounds(
+      xAxisType,
+      truncateXAxis,
+      xAxisMin,
+      xAxisMax,
+      seriesType,
+    ),
   };
 
   let yAxis: any = {
diff --git a/superset-frontend/plugins/plugin-chart-echarts/src/utils/controls.ts b/superset-frontend/plugins/plugin-chart-echarts/src/utils/controls.ts
index 67a5414112..689a6e99e5 100644
--- a/superset-frontend/plugins/plugin-chart-echarts/src/utils/controls.ts
+++ b/superset-frontend/plugins/plugin-chart-echarts/src/utils/controls.ts
@@ -19,7 +19,6 @@
 
 import { validateNumber } from '@superset-ui/core';
 
-// eslint-disable-next-line import/prefer-default-export
 export function parseAxisBound(
   bound?: string | number | null,
 ): number | undefined {
diff --git a/superset-frontend/plugins/plugin-chart-echarts/src/utils/series.ts b/superset-frontend/plugins/plugin-chart-echarts/src/utils/series.ts
index 69c0ccbe1b..a294fa44c3 100644
--- a/superset-frontend/plugins/plugin-chart-echarts/src/utils/series.ts
+++ b/superset-frontend/plugins/plugin-chart-echarts/src/utils/series.ts
@@ -41,7 +41,12 @@ import {
   StackControlsValue,
   TIMESERIES_CONSTANTS,
 } from '../constants';
-import { LegendOrientation, LegendType, StackType } from '../types';
+import {
+  EchartsTimeseriesSeriesType,
+  LegendOrientation,
+  LegendType,
+  StackType,
+} from '../types';
 import { defaultLegendPadding } from '../defaults';
 
 function isDefined<T>(value: T | undefined | null): boolean {
@@ -547,16 +552,35 @@ export function calculateLowerLogTick(minPositiveValue: number) {
   return Math.pow(10, logBase10);
 }
 
+type BoundsType = {
+  min?: number | 'dataMin';
+  max?: number | 'dataMax';
+  scale?: true;
+};
+
 export function getMinAndMaxFromBounds(
   axisType: AxisType,
   truncateAxis: boolean,
   min?: number,
   max?: number,
-): { min: number | 'dataMin'; max: number | 'dataMax' } | {} {
-  return truncateAxis && axisType === AxisType.value
-    ? {
-        min: min === undefined ? 'dataMin' : min,
-        max: max === undefined ? 'dataMax' : max,
-      }
-    : {};
+  seriesType?: EchartsTimeseriesSeriesType,
+): BoundsType | {} {
+  if (axisType === AxisType.value && truncateAxis) {
+    const ret: BoundsType = {};
+    if (seriesType === EchartsTimeseriesSeriesType.Bar) {
+      ret.scale = true;
+    }
+    if (min !== undefined) {
+      ret.min = min;
+    } else if (seriesType !== EchartsTimeseriesSeriesType.Bar) {
+      ret.min = 'dataMin';
+    }
+    if (max !== undefined) {
+      ret.max = max;
+    } else if (seriesType !== EchartsTimeseriesSeriesType.Bar) {
+      ret.max = 'dataMax';
+    }
+    return ret;
+  }
+  return {};
 }
diff --git a/superset-frontend/plugins/plugin-chart-echarts/test/utils/series.test.ts b/superset-frontend/plugins/plugin-chart-echarts/test/utils/series.test.ts
index b309bf6f3c..8e55b125b8 100644
--- a/superset-frontend/plugins/plugin-chart-echarts/test/utils/series.test.ts
+++ b/superset-frontend/plugins/plugin-chart-echarts/test/utils/series.test.ts
@@ -41,7 +41,11 @@ import {
   sortAndFilterSeries,
   sortRows,
 } from '../../src/utils/series';
-import { LegendOrientation, LegendType } from '../../src/types';
+import {
+  EchartsTimeseriesSeriesType,
+  LegendOrientation,
+  LegendType,
+} from '../../src/types';
 import { defaultLegendPadding } from '../../src/defaults';
 import { NULL_STRING } from '../../src/constants';
 
@@ -885,28 +889,109 @@ test('getAxisType', () => {
 });
 
 test('getMinAndMaxFromBounds returns empty object when not truncating', () => {
-  expect(getMinAndMaxFromBounds(AxisType.value, false, 10, 100)).toEqual({});
+  expect(
+    getMinAndMaxFromBounds(
+      AxisType.value,
+      false,
+      10,
+      100,
+      EchartsTimeseriesSeriesType.Bar,
+    ),
+  ).toEqual({});
+});
+
+test('getMinAndMaxFromBounds returns empty object for categorical axis', () => {
+  expect(
+    getMinAndMaxFromBounds(
+      AxisType.category,
+      false,
+      10,
+      100,
+      EchartsTimeseriesSeriesType.Bar,
+    ),
+  ).toEqual({});
+});
+
+test('getMinAndMaxFromBounds returns empty object for time axis', () => {
+  expect(
+    getMinAndMaxFromBounds(
+      AxisType.time,
+      false,
+      10,
+      100,
+      EchartsTimeseriesSeriesType.Bar,
+    ),
+  ).toEqual({});
 });
 
-test('getMinAndMaxFromBounds returns automatic bounds when truncating', () => {
+test('getMinAndMaxFromBounds returns dataMin/dataMax for non-bar charts', () => {
   expect(
-    getMinAndMaxFromBounds(AxisType.value, true, undefined, undefined),
+    getMinAndMaxFromBounds(
+      AxisType.value,
+      true,
+      undefined,
+      undefined,
+      EchartsTimeseriesSeriesType.Line,
+    ),
   ).toEqual({
     min: 'dataMin',
     max: 'dataMax',
   });
 });
 
-test('getMinAndMaxFromBounds returns automatic upper bound when truncating', () => {
-  expect(getMinAndMaxFromBounds(AxisType.value, true, 10, undefined)).toEqual({
+test('getMinAndMaxFromBounds returns bound without scale for non-bar charts', () => {
+  expect(
+    getMinAndMaxFromBounds(
+      AxisType.value,
+      true,
+      10,
+      undefined,
+      EchartsTimeseriesSeriesType.Line,
+    ),
+  ).toEqual({
     min: 10,
     max: 'dataMax',
   });
 });
 
+test('getMinAndMaxFromBounds returns scale when truncating without bounds', () => {
+  expect(
+    getMinAndMaxFromBounds(
+      AxisType.value,
+      true,
+      undefined,
+      undefined,
+      EchartsTimeseriesSeriesType.Bar,
+    ),
+  ).toEqual({ scale: true });
+});
+
+test('getMinAndMaxFromBounds returns automatic upper bound when truncating', () => {
+  expect(
+    getMinAndMaxFromBounds(
+      AxisType.value,
+      true,
+      10,
+      undefined,
+      EchartsTimeseriesSeriesType.Bar,
+    ),
+  ).toEqual({
+    min: 10,
+    scale: true,
+  });
+});
+
 test('getMinAndMaxFromBounds returns automatic lower bound when truncating', () => {
-  expect(getMinAndMaxFromBounds(AxisType.value, true, undefined, 100)).toEqual({
-    min: 'dataMin',
+  expect(
+    getMinAndMaxFromBounds(
+      AxisType.value,
+      true,
+      undefined,
+      100,
+      EchartsTimeseriesSeriesType.Bar,
+    ),
+  ).toEqual({
     max: 100,
+    scale: true,
   });
 });