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/08 14:14:39 UTC

(superset) branch 3.1 updated (5bcd3ef17e -> d0961d0ed8)

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 5bcd3ef17e chore: harmonize and clean up list views (#25961)
     new 96c0497fa9 fix(menu): Styling active menu in SPA navigation (#25533)
     new 0925d75dfa fix(embedded): Hide sensitive payload data from guest users (#25878)
     new 2104a9a853 fix(init-job): Fix envFrom for init job in helm chart (#26157)
     new 5c24c580dd chore: Lower giveup log level for retried functions to warning (#26188)
     new fb50819fcd fix: Includes 90° x-axis label rotation (#26207)
     new 77c73b63db fix(sqllab): flaky json explore modal due to over-rendering (#26156)
     new 6fa75b7047 chore: Adds note about numerical x-axis (#26208)
     new ec0a338aa3 fix(dashboard): use textContent to render hidden title (#26189)
     new c2612d8c26 fix: support custom links in markdown (#26211)
     new b699df7030 fix(chart-filter): Avoid column denormalization if not enabled (#26199)
     new d0961d0ed8 fix(plugin-chart-echarts): support truncated numeric x-axis (#26215)

The 11 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:
 UPDATING.md                                        |   1 +
 helm/superset/Chart.yaml                           |   2 +-
 helm/superset/README.md                            |   2 +-
 helm/superset/templates/init-job.yaml              |   2 +-
 .../src/components/SafeMarkdown.tsx                |   1 +
 .../plugin-chart-echarts/src/Bubble/constants.ts   |   1 +
 .../src/Bubble/controlPanel.tsx                    |   5 +-
 .../src/Bubble/transformProps.ts                   |  17 +-
 .../src/MixedTimeseries/controlPanel.tsx           |   1 +
 .../src/MixedTimeseries/transformProps.ts          |   6 +-
 .../src/Timeseries/Area/controlPanel.tsx           |   5 +
 .../src/Timeseries/Regular/Bar/controlPanel.tsx    |   5 +
 .../src/Timeseries/Regular/Line/controlPanel.tsx   |   5 +
 .../Timeseries/Regular/Scatter/controlPanel.tsx    |   5 +
 .../Timeseries/Regular/SmoothLine/controlPanel.tsx |   5 +
 .../src/Timeseries/Step/controlPanel.tsx           |   5 +
 .../src/Timeseries/constants.ts                    |   1 +
 .../src/Timeseries/transformProps.ts               |  28 +-
 .../plugin-chart-echarts/src/Timeseries/types.ts   |   2 +
 .../plugins/plugin-chart-echarts/src/controls.tsx  |  31 ++
 .../plugin-chart-echarts/src/utils/controls.ts     |   2 +-
 .../plugin-chart-echarts/src/utils/series.ts       |  14 +
 .../test/Bubble/transformProps.test.ts             |   1 +
 .../test/utils/controls.test.ts                    |  22 +-
 .../plugin-chart-echarts/test/utils/series.test.ts |  28 ++
 .../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 +++-
 .../{SouthPane.test.tsx => Results.test.tsx}       |  87 +++---
 .../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 +-
 .../src/components/DynamicEditableTitle/index.tsx  |   5 +-
 superset-frontend/src/features/home/Menu.tsx       |  37 ++-
 .../src/hooks/apiResources/dashboards.ts           |   1 +
 superset/dashboards/schemas.py                     |  20 +-
 superset/datasource/api.py                         |   5 +-
 superset/models/helpers.py                         |  17 +-
 superset/utils/retries.py                          |   3 +
 tests/integration_tests/dashboards/api_tests.py    |  43 +++
 46 files changed, 858 insertions(+), 371 deletions(-)
 copy superset-frontend/src/SqlLab/components/SouthPane/{SouthPane.test.tsx => Results.test.tsx} (60%)
 create mode 100644 superset-frontend/src/SqlLab/components/SouthPane/Results.tsx


(superset) 08/11: fix(dashboard): use textContent to render hidden title (#26189)

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 ec0a338aa3d66f3a5edc4199f6309b32faac91d9
Author: ʈᵃᵢ <td...@gmail.com>
AuthorDate: Thu Dec 7 13:33:49 2023 -0800

    fix(dashboard): use textContent to render hidden title (#26189)
    
    (cherry picked from commit 88fb3428872a332c750187e15cdc58397231f396)
---
 superset-frontend/src/components/DynamicEditableTitle/index.tsx | 5 +----
 1 file changed, 1 insertion(+), 4 deletions(-)

diff --git a/superset-frontend/src/components/DynamicEditableTitle/index.tsx b/superset-frontend/src/components/DynamicEditableTitle/index.tsx
index 86205bebc2..670962de5f 100644
--- a/superset-frontend/src/components/DynamicEditableTitle/index.tsx
+++ b/superset-frontend/src/components/DynamicEditableTitle/index.tsx
@@ -113,10 +113,7 @@ export const DynamicEditableTitle = ({
   // then we can measure the width of that span to resize the input element
   useLayoutEffect(() => {
     if (sizerRef?.current) {
-      sizerRef.current.innerHTML = (currentTitle || placeholder).replace(
-        /\s/g,
-        '&nbsp;',
-      );
+      sizerRef.current.textContent = currentTitle || placeholder;
     }
   }, [currentTitle, placeholder, sizerRef]);
 


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

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 77c73b63db8fe9574db129e85b4393fa33f283a6
Author: JUST.in DO IT <ju...@airbnb.com>
AuthorDate: Thu Dec 7 09:28:59 2023 -0800

    fix(sqllab): flaky json explore modal due to over-rendering (#26156)
    
    (cherry picked from commit f30f685eb5791026ebd06f5fb034dbd262ef9d4c)
---
 .../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 +++-
 .../{SouthPane.test.tsx => Results.test.tsx}       |  87 +++---
 .../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, 580 insertions(+), 321 deletions(-)

diff --git a/superset-frontend/src/SqlLab/components/QueryAutoRefresh/index.tsx b/superset-frontend/src/SqlLab/components/QueryAutoRefresh/index.tsx
index f4808f52fd..a2ffcf85cb 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);
+  runningQueryStateList.includes(q?.state) && !q?.resultsKey;
 
 // 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 6fd84a0d2a..ad1881b5d9 100644
--- a/superset-frontend/src/SqlLab/components/QueryHistory/QueryHistory.test.tsx
+++ b/superset-frontend/src/SqlLab/components/QueryHistory/QueryHistory.test.tsx
@@ -19,9 +19,10 @@
 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 = {
-  queries: [],
+  queryEditorId: 123,
   displayLimit: 1000,
   latestQueryId: 'yhMUZCGb',
 };
@@ -32,7 +33,7 @@ const setup = (overrides = {}) => (
 
 describe('QueryHistory', () => {
   it('Renders an empty state for query history', () => {
-    render(setup());
+    render(setup(), { useRedux: true, initialState });
 
     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 cab1160144..311a125d55 100644
--- a/superset-frontend/src/SqlLab/components/QueryHistory/index.tsx
+++ b/superset-frontend/src/SqlLab/components/QueryHistory/index.tsx
@@ -16,13 +16,15 @@
  * specific language governing permissions and limitations
  * under the License.
  */
-import React from 'react';
+import React, { useMemo } from 'react';
+import { shallowEqual, useSelector } from 'react-redux';
 import { EmptyStateMedium } from 'src/components/EmptyState';
-import { t, styled, QueryResponse } from '@superset-ui/core';
+import { t, styled } from '@superset-ui/core';
 import QueryTable from 'src/SqlLab/components/QueryTable';
+import { SqlLabRootState } from 'src/SqlLab/types';
 
 interface QueryHistoryProps {
-  queries: QueryResponse[];
+  queryEditorId: string | number;
   displayLimit: number;
   latestQueryId: string | undefined;
 }
@@ -39,11 +41,23 @@ const StyledEmptyStateWrapper = styled.div`
 `;
 
 const QueryHistory = ({
-  queries,
+  queryEditorId,
   displayLimit,
   latestQueryId,
-}: QueryHistoryProps) =>
-  queries.length > 0 ? (
+}: 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 ? (
     <QueryTable
       columns={[
         'state',
@@ -55,7 +69,7 @@ const QueryHistory = ({
         'results',
         'actions',
       ]}
-      queries={queries}
+      queries={editorQueries}
       displayLimit={displayLimit}
       latestQueryId={latestQueryId}
     />
@@ -67,5 +81,6 @@ 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 5dc8a43c19..3282a939ef 100644
--- a/superset-frontend/src/SqlLab/components/QueryTable/index.tsx
+++ b/superset-frontend/src/SqlLab/components/QueryTable/index.tsx
@@ -251,8 +251,7 @@ const QueryTable = ({
               modalBody={
                 <ResultSet
                   showSql
-                  user={user}
-                  query={query}
+                  queryId={query.id}
                   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 d823c586f7..e5844fed5c 100644
--- a/superset-frontend/src/SqlLab/components/ResultSet/ResultSet.test.tsx
+++ b/superset-frontend/src/SqlLab/components/ResultSet/ResultSet.test.tsx
@@ -37,65 +37,91 @@ import {
 
 const mockedProps = {
   cache: true,
-  query: queries[0],
+  queryId: queries[0].id,
   height: 140,
   database: { allows_virtual_table_explore: true },
-  user,
+  displayLimit: 1000,
   defaultQueryLimit: 1000,
 };
-const stoppedQueryProps = { ...mockedProps, query: stoppedQuery };
-const runningQueryProps = { ...mockedProps, query: runningQuery };
-const fetchingQueryProps = {
-  ...mockedProps,
-  query: {
-    dbId: 1,
-    cached: false,
-    ctas: false,
-    id: 'ryhHUZCGb',
-    progress: 100,
-    state: 'fetching',
-    startDttm: Date.now() - 500,
+const stoppedQueryState = {
+  ...initialState,
+  sqlLab: {
+    ...initialState.sqlLab,
+    queries: {
+      [stoppedQuery.id]: stoppedQuery,
+    },
   },
 };
-const cachedQueryProps = { ...mockedProps, query: cachedQuery };
-const failedQueryWithErrorMessageProps = {
-  ...mockedProps,
-  query: failedQueryWithErrorMessage,
-};
-const failedQueryWithErrorsProps = {
-  ...mockedProps,
-  query: failedQueryWithErrors,
+const runningQueryState = {
+  ...initialState,
+  sqlLab: {
+    ...initialState.sqlLab,
+    queries: {
+      [runningQuery.id]: runningQuery,
+    },
+  },
 };
-const newProps = {
-  query: {
-    cached: false,
-    resultsKey: 'new key',
-    results: {
-      data: [{ a: 1 }],
+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 asyncQueryProps = {
-  ...mockedProps,
-  database: { allow_run_async: true },
+const cachedQueryState = {
+  ...initialState,
+  sqlLab: {
+    ...initialState.sqlLab,
+    queries: {
+      [cachedQuery.id]: cachedQuery,
+    },
+  },
 };
-const asyncRefetchDataPreviewProps = {
-  ...asyncQueryProps,
-  query: {
-    state: 'success',
-    results: undefined,
-    isDataPreview: true,
+const failedQueryWithErrorMessageState = {
+  ...initialState,
+  sqlLab: {
+    ...initialState.sqlLab,
+    queries: {
+      [failedQueryWithErrorMessage.id]: failedQueryWithErrorMessage,
+    },
   },
 };
-const asyncRefetchResultsTableProps = {
-  ...asyncQueryProps,
-  query: {
-    state: 'success',
-    results: undefined,
-    resultsKey: 'async results key',
+const failedQueryWithErrorsState = {
+  ...initialState,
+  sqlLab: {
+    ...initialState.sqlLab,
+    queries: {
+      [failedQueryWithErrors.id]: failedQueryWithErrors,
+    },
   },
 };
+
+const newProps = {
+  displayLimit: 1001,
+};
+const asyncQueryProps = {
+  ...mockedProps,
+  database: { allow_run_async: true },
+};
+
+const reRunQueryEndpoint = 'glob:*/api/v1/sqllab/execute/';
 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);
@@ -107,25 +133,47 @@ const setup = (props?: any, store?: Store) =>
 
 describe('ResultSet', () => {
   test('renders a Table', async () => {
-    const { getByTestId } = setup(mockedProps, mockStore(initialState));
+    const { getByTestId } = setup(
+      mockedProps,
+      mockStore({
+        ...initialState,
+        user,
+        sqlLab: {
+          ...initialState.sqlLab,
+          queries: {
+            [queries[0].id]: queries[0],
+          },
+        },
+      }),
+    );
     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),
+      mockStore({
+        ...initialState,
+        user,
+        sqlLab: {
+          ...initialState.sqlLab,
+          queries: {
+            [query.id]: query,
+          },
+        },
+      }),
     );
 
     const table = getByTestId('table-container');
     expect(table).toBeInTheDocument();
 
     const firstColumn = queryAllByText(
-      mockedProps.query.results?.columns[0].column_name ?? '',
+      query.results?.columns[0].column_name ?? '',
     )[0];
     const secondColumn = queryAllByText(
-      mockedProps.query.results?.columns[1].column_name ?? '',
+      query.results?.columns[1].column_name ?? '',
     )[0];
     expect(firstColumn).toBeInTheDocument();
     expect(secondColumn).toBeInTheDocument();
@@ -135,12 +183,24 @@ describe('ResultSet', () => {
   });
 
   test('should render empty results', async () => {
-    const props = {
-      ...mockedProps,
-      query: { ...mockedProps.query, results: { data: [] } },
+    const query = {
+      ...queries[0],
+      results: { data: [] },
     };
     await waitFor(() => {
-      setup(props, mockStore(initialState));
+      setup(
+        mockedProps,
+        mockStore({
+          ...initialState,
+          user,
+          sqlLab: {
+            ...initialState.sqlLab,
+            queries: {
+              [query.id]: query,
+            },
+          },
+        }),
+      );
     });
 
     const alert = screen.getByRole('alert');
@@ -149,42 +209,70 @@ describe('ResultSet', () => {
   });
 
   test('should call reRunQuery if timed out', async () => {
-    const store = mockStore(initialState);
-    const propsWithError = {
-      ...mockedProps,
-      query: { ...queries[0], errorMessage: 'Your session timed out' },
+    const query = {
+      ...queries[0],
+      errorMessage: 'Your session timed out',
     };
+    const store = mockStore({
+      ...initialState,
+      user,
+      sqlLab: {
+        ...initialState.sqlLab,
+        queries: {
+          [query.id]: query,
+        },
+      },
+    });
 
-    setup(propsWithError, store);
+    expect(fetchMock.calls(reRunQueryEndpoint)).toHaveLength(0);
+    setup(mockedProps, 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 store = mockStore(initialState);
+    const query = queries[0];
+    const store = mockStore({
+      ...initialState,
+      user,
+      sqlLab: {
+        ...initialState.sqlLab,
+        queries: {
+          [query.id]: query,
+        },
+      },
+    });
     setup(mockedProps, store);
     expect(store.getActions()).toEqual([]);
+    expect(fetchMock.calls(reRunQueryEndpoint)).toHaveLength(0);
   });
 
   test('should render cached query', async () => {
-    const store = mockStore(initialState);
-    const { rerender } = setup(cachedQueryProps, store);
+    const store = mockStore(cachedQueryState);
+    const { rerender } = setup(
+      { ...mockedProps, queryId: cachedQuery.id },
+      store,
+    );
 
     // @ts-ignore
-    rerender(<ResultSet {...newProps} />);
-    expect(store.getActions()).toHaveLength(2);
-    expect(store.getActions()[0].query.results).toEqual(
-      cachedQueryProps.query.results,
-    );
+    rerender(<ResultSet {...mockedProps} {...newProps} />);
+    expect(store.getActions()).toHaveLength(1);
+    expect(store.getActions()[0].query.results).toEqual(cachedQuery.results);
     expect(store.getActions()[0].type).toEqual('CLEAR_QUERY_RESULTS');
   });
 
   test('should render stopped query', async () => {
     await waitFor(() => {
-      setup(stoppedQueryProps, mockStore(initialState));
+      setup(
+        { ...mockedProps, queryId: stoppedQuery.id },
+        mockStore(stoppedQueryState),
+      );
     });
 
     const alert = screen.getByRole('alert');
@@ -192,15 +280,18 @@ describe('ResultSet', () => {
   });
 
   test('should render running/pending/fetching query', async () => {
-    const { getByTestId } = setup(runningQueryProps, mockStore(initialState));
+    const { getByTestId } = setup(
+      { ...mockedProps, queryId: runningQuery.id },
+      mockStore(runningQueryState),
+    );
     const progressBar = getByTestId('progress-bar');
     expect(progressBar).toBeInTheDocument();
   });
 
   test('should render fetching w/ 100 progress query', async () => {
     const { getByRole, getByText } = setup(
-      fetchingQueryProps,
-      mockStore(initialState),
+      mockedProps,
+      mockStore(fetchingQueryState),
     );
     const loading = getByRole('status');
     expect(loading).toBeInTheDocument();
@@ -209,7 +300,10 @@ describe('ResultSet', () => {
 
   test('should render a failed query with an error message', async () => {
     await waitFor(() => {
-      setup(failedQueryWithErrorMessageProps, mockStore(initialState));
+      setup(
+        { ...mockedProps, queryId: failedQueryWithErrorMessage.id },
+        mockStore(failedQueryWithErrorMessageState),
+      );
     });
 
     expect(screen.getByText('Database error')).toBeInTheDocument();
@@ -218,44 +312,129 @@ describe('ResultSet', () => {
 
   test('should render a failed query with an errors object', async () => {
     await waitFor(() => {
-      setup(failedQueryWithErrorsProps, mockStore(initialState));
+      setup(
+        { ...mockedProps, queryId: failedQueryWithErrors.id },
+        mockStore(failedQueryWithErrorsState),
+      );
     });
     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, query: queryWithNoQueryLimit };
-    const { getByRole } = setup(props, mockStore(initialState));
+    const props = { ...mockedProps, queryId: queryWithNoQueryLimit.id };
+    const { getByRole } = setup(
+      props,
+      mockStore({
+        ...initialState,
+        user,
+        sqlLab: {
+          ...initialState.sqlLab,
+          queries: {
+            [queryWithNoQueryLimit.id]: queryWithNoQueryLimit,
+          },
+        },
+      }),
+    );
     expect(getByRole('table')).toBeInTheDocument();
   });
 
   test('Async queries - renders "Fetch data preview" button when data preview has no results', () => {
-    setup(asyncRefetchDataPreviewProps, mockStore(initialState));
+    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,
+          },
+        },
+      }),
+    );
     expect(
       screen.getByRole('button', {
         name: /fetch data preview/i,
       }),
     ).toBeVisible();
-    expect(screen.queryByRole('grid')).toBe(null);
+    expect(screen.queryByRole('table')).toBe(null);
   });
 
   test('Async queries - renders "Refetch results" button when a query has no results', () => {
-    setup(asyncRefetchResultsTableProps, mockStore(initialState));
+    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,
+          },
+        },
+      }),
+    );
     expect(
       screen.getByRole('button', {
         name: /refetch results/i,
       }),
     ).toBeVisible();
-    expect(screen.queryByRole('grid')).toBe(null);
+    expect(screen.queryByRole('table')).toBe(null);
   });
 
   test('Async queries - renders on the first call', () => {
-    setup(asyncQueryProps, mockStore(initialState));
+    const query = {
+      ...queries[0],
+    };
+    setup(
+      { ...asyncQueryProps, queryId: query.id },
+      mockStore({
+        ...initialState,
+        user,
+        sqlLab: {
+          ...initialState.sqlLab,
+          queries: {
+            [query.id]: query,
+          },
+        },
+      }),
+    );
     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 58e55a1df7..69e4508434 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 { useDispatch } from 'react-redux';
+import { shallowEqual, useDispatch, useSelector } 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,8 +41,7 @@ import {
   ISimpleColumn,
   SaveDatasetModal,
 } from 'src/SqlLab/components/SaveDatasetModal';
-import { UserWithPermissionsAndRoles } from 'src/types/bootstrapTypes';
-import { EXPLORE_CHART_DEFAULT } from 'src/SqlLab/types';
+import { EXPLORE_CHART_DEFAULT, SqlLabRootState } from 'src/SqlLab/types';
 import { mountExploreUrl } from 'src/explore/exploreUtils';
 import { postFormData } from 'src/explore/exploreUtils/formData';
 import ProgressBar from 'src/components/ProgressBar';
@@ -82,12 +81,11 @@ export interface ResultSetProps {
   database?: Record<string, any>;
   displayLimit: number;
   height: number;
-  query: QueryResponse;
+  queryId: string;
   search?: boolean;
   showSql?: boolean;
   showSqlInline?: boolean;
   visualize?: boolean;
-  user: UserWithPermissionsAndRoles;
   defaultQueryLimit: number;
 }
 
@@ -145,14 +143,44 @@ const ResultSet = ({
   database = {},
   displayLimit,
   height,
-  query,
+  queryId,
   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;
@@ -179,8 +207,8 @@ const ResultSet = ({
     reRunQueryIfSessionTimeoutErrorOnMount();
   }, [reRunQueryIfSessionTimeoutErrorOnMount]);
 
-  const fetchResults = (query: QueryResponse) => {
-    dispatch(fetchQueryResults(query, displayLimit));
+  const fetchResults = (q: typeof query) => {
+    dispatch(fetchQueryResults(q, displayLimit));
   };
 
   const prevQuery = usePrevious(query);
@@ -479,7 +507,7 @@ const ResultSet = ({
       <ResultlessStyles>
         <ErrorMessageWithStackTrace
           title={t('Database error')}
-          error={query?.errors?.[0]}
+          error={query?.extra?.errors?.[0] || query?.errors?.[0]}
           subtitle={<MonospaceDiv>{query.errorMessage}</MonospaceDiv>}
           copyText={query.errorMessage || undefined}
           link={query.link}
@@ -662,4 +690,4 @@ const ResultSet = ({
   );
 };
 
-export default ResultSet;
+export default React.memo(ResultSet);
diff --git a/superset-frontend/src/SqlLab/components/SouthPane/SouthPane.test.tsx b/superset-frontend/src/SqlLab/components/SouthPane/Results.test.tsx
similarity index 60%
copy from superset-frontend/src/SqlLab/components/SouthPane/SouthPane.test.tsx
copy to superset-frontend/src/SqlLab/components/SouthPane/Results.test.tsx
index 80a102ff21..c70c039fe5 100644
--- a/superset-frontend/src/SqlLab/components/SouthPane/SouthPane.test.tsx
+++ b/superset-frontend/src/SqlLab/components/SouthPane/Results.test.tsx
@@ -17,15 +17,11 @@
  * under the License.
  */
 import React from 'react';
-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 { render } from 'spec/helpers/testing-library';
 import { initialState, table, defaultQueryEditor } from 'src/SqlLab/fixtures';
 import { denormalizeTimestamp } from '@superset-ui/core';
-import { Store } from 'redux';
+import { LOCALSTORAGE_MAX_QUERY_AGE_MS } from 'src/SqlLab/constants';
+import Results from './Results';
 
 const mockedProps = {
   queryEditorId: defaultQueryEditor.id,
@@ -37,19 +33,21 @@ const mockedProps = {
 
 const mockedEmptyProps = {
   queryEditorId: 'random_id',
-  latestQueryId: '',
+  latestQueryId: 'empty_query_id',
   height: 100,
   displayLimit: 100,
   defaultQueryLimit: 100,
 };
 
-jest.mock('src/SqlLab/components/SqlEditorLeftBar', () => jest.fn());
+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 middlewares = [thunk];
-const mockStore = configureStore(middlewares);
-const store = mockStore({
+const mockState = {
   ...initialState,
   sqlLab: {
     ...initialState,
@@ -72,6 +70,7 @@ const store = mockStore({
         startDttm: Date.now(),
         sqlEditorId: defaultQueryEditor.id,
         extra: { progress: latestQueryProgressMsg },
+        sql: 'select * from table1',
       },
       lXJa7F9_r: {
         cached: false,
@@ -81,6 +80,7 @@ const store = mockStore({
         id: 'lXJa7F9_r',
         startDttm: 1559238500401,
         sqlEditorId: defaultQueryEditor.id,
+        sql: 'select * from table2',
       },
       '2g2_iRFMl': {
         cached: false,
@@ -90,53 +90,46 @@ const store = mockStore({
         id: '2g2_iRFMl',
         startDttm: 1559238506925,
         sqlEditorId: defaultQueryEditor.id,
+        sql: 'select * from table3',
       },
-      erWdqEWPm: {
+      expired_query_id: {
         cached: false,
-        changed_on: denormalizeTimestamp(new Date(1559238516395).toISOString()),
+        changed_on: denormalizeTimestamp(
+          new Date(expireDateTime).toISOString(),
+        ),
         db: 'main',
         dbId: 1,
-        id: 'erWdqEWPm',
-        startDttm: 1559238516395,
+        id: 'expired_query_id',
+        startDttm: expireDateTime,
         sqlEditorId: defaultQueryEditor.id,
+        sql: 'select * from table4',
       },
     },
   },
-});
-const setup = (props: SouthPaneProps, store: Store) =>
-  render(<SouthPane {...props} />, {
-    useRedux: true,
-    ...(store && { store }),
-  });
-
-describe('SouthPane', () => {
-  const renderAndWait = (props: SouthPaneProps, store: Store) =>
-    waitFor(async () => setup(props, store));
+};
 
-  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();
+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();
+});
 
-  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();
+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();
+});
 
-  it('should pass latest query down to ResultSet component', async () => {
-    await renderAndWait(mockedProps, store);
-
-    expect(screen.getByText(latestQueryProgressMsg)).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
new file mode 100644
index 0000000000..4e1b6219ae
--- /dev/null
+++ b/superset-frontend/src/SqlLab/components/SouthPane/Results.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 { 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 80a102ff21..c978a4ca32 100644
--- a/superset-frontend/src/SqlLab/components/SouthPane/SouthPane.test.tsx
+++ b/superset-frontend/src/SqlLab/components/SouthPane/SouthPane.test.tsx
@@ -17,15 +17,12 @@
  * under the License.
  */
 import React from 'react';
-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 { render } from 'spec/helpers/testing-library';
+import SouthPane 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,
@@ -37,29 +34,32 @@ const mockedProps = {
 
 const mockedEmptyProps = {
   queryEditorId: 'random_id',
-  latestQueryId: '',
+  latestQueryId: 'empty_query_id',
   height: 100,
   displayLimit: 100,
   defaultQueryLimit: 100,
 };
 
-jest.mock('src/SqlLab/components/SqlEditorLeftBar', () => jest.fn());
-
 const latestQueryProgressMsg = 'LATEST QUERY MESSAGE - LCly_kkIN';
 
-const middlewares = [thunk];
-const mockStore = configureStore(middlewares);
-const store = mockStore({
+const mockState = {
   ...initialState,
   sqlLab: {
-    ...initialState,
+    ...initialState.sqlLab,
     offline: false,
     tables: [
       {
         ...table,
+        name: 'table3',
         dataPreviewQueryId: '2g2_iRFMl',
         queryEditorId: defaultQueryEditor.id,
       },
+      {
+        ...table,
+        name: 'table4',
+        dataPreviewQueryId: 'erWdqEWPm',
+        queryEditorId: defaultQueryEditor.id,
+      },
     ],
     databases: {},
     queries: {
@@ -72,6 +72,7 @@ const store = mockStore({
         startDttm: Date.now(),
         sqlEditorId: defaultQueryEditor.id,
         extra: { progress: latestQueryProgressMsg },
+        sql: 'select * from table1',
       },
       lXJa7F9_r: {
         cached: false,
@@ -81,6 +82,7 @@ const store = mockStore({
         id: 'lXJa7F9_r',
         startDttm: 1559238500401,
         sqlEditorId: defaultQueryEditor.id,
+        sql: 'select * from table2',
       },
       '2g2_iRFMl': {
         cached: false,
@@ -90,6 +92,7 @@ const store = mockStore({
         id: '2g2_iRFMl',
         startDttm: 1559238506925,
         sqlEditorId: defaultQueryEditor.id,
+        sql: 'select * from table3',
       },
       erWdqEWPm: {
         cached: false,
@@ -99,44 +102,38 @@ const store = mockStore({
         id: 'erWdqEWPm',
         startDttm: 1559238516395,
         sqlEditorId: defaultQueryEditor.id,
+        sql: 'select * from table4',
       },
     },
   },
-});
-const setup = (props: SouthPaneProps, store: Store) =>
-  render(<SouthPane {...props} />, {
-    useRedux: true,
-    ...(store && { store }),
-  });
-
-describe('SouthPane', () => {
-  const renderAndWait = (props: SouthPaneProps, store: Store) =>
-    waitFor(async () => setup(props, store));
+};
 
-  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();
+test('should render offline when the state is offline', async () => {
+  const { getByText } = render(<SouthPane {...mockedEmptyProps} />, {
+    useRedux: true,
+    initialState: {
+      ...initialState,
+      sqlLab: {
+        ...initialState.sqlLab,
+        offline: true,
+      },
+    },
   });
 
-  it('should render offline when the state is offline', async () => {
-    await renderAndWait(
-      mockedEmptyProps,
-      mockStore({
-        ...initialState,
-        sqlLab: {
-          ...initialState.sqlLab,
-          offline: true,
-        },
-      }),
-    );
+  expect(getByText(STATUS_OPTIONS.offline)).toBeVisible();
+});
 
-    expect(screen.getByText(STATUS_OPTIONS.offline)).toBeVisible();
+test('should render tabs for table preview queries', () => {
+  const { getAllByRole } = render(<SouthPane {...mockedProps} />, {
+    useRedux: true,
+    initialState: mockState,
   });
 
-  it('should pass latest query down to ResultSet component', async () => {
-    await renderAndWait(mockedProps, store);
-
-    expect(screen.getByText(latestQueryProgressMsg)).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}\``);
   });
 });
diff --git a/superset-frontend/src/SqlLab/components/SouthPane/index.tsx b/superset-frontend/src/SqlLab/components/SouthPane/index.tsx
index 38a20f9f6d..0bbce99b1c 100644
--- a/superset-frontend/src/SqlLab/components/SouthPane/index.tsx
+++ b/superset-frontend/src/SqlLab/components/SouthPane/index.tsx
@@ -19,10 +19,8 @@
 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 { EmptyStateMedium } from 'src/components/EmptyState';
-import { FeatureFlag, styled, t, isFeatureEnabled } from '@superset-ui/core';
+import { styled, t } from '@superset-ui/core';
 
 import { setActiveSouthPaneTab } from 'src/SqlLab/actions/sqlLab';
 
@@ -33,11 +31,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 = 140;
+const TAB_HEIGHT = 130;
 
 /*
     editorQueries are queries executed by users passed from SqlEditor component
@@ -85,18 +83,6 @@ 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,
@@ -105,128 +91,43 @@ const SouthPane = ({
   defaultQueryLimit,
 }: SouthPaneProps) => {
   const dispatch = useDispatch();
-  const user = useSelector(({ user }: SqlLabRootState) => user, shallowEqual);
-  const { databases, offline, queries, tables } = useSelector(
-    ({ sqlLab: { databases, offline, queries, tables } }: SqlLabRootState) => ({
-      databases,
+  const { offline, tables } = useSelector(
+    ({ sqlLab: { offline, tables } }: SqlLabRootState) => ({
       offline,
-      queries,
       tables,
     }),
     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 queries = useSelector(
+    ({ sqlLab: { queries } }: SqlLabRootState) => Object.keys(queries),
+    shallowEqual,
   );
-
   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));
   };
-  const renderOfflineStatus = () => (
+
+  return offline ? (
     <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"
@@ -243,16 +144,41 @@ const SouthPane = ({
         animated={false}
       >
         <Tabs.TabPane tab={t('Results')} key="Results">
-          {renderResults()}
+          {latestQueryId && (
+            <Results
+              height={innerTabContentHeight}
+              latestQueryId={latestQueryId}
+              displayLimit={displayLimit}
+              defaultQueryLimit={defaultQueryLimit}
+            />
+          )}
         </Tabs.TabPane>
         <Tabs.TabPane tab={t('Query history')} key="History">
           <QueryHistory
-            queries={editorQueries}
+            queryEditorId={queryEditorId}
             displayLimit={displayLimit}
             latestQueryId={latestQueryId}
           />
         </Tabs.TabPane>
-        {renderDataPreviewTabs()}
+        {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>
+            ),
+        )}
       </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 63f67170d0..6a25492ce5 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 jest.Mock).mockClear();
-    (ResultSet as jest.Mock).mockImplementation(() => (
+    (ResultSet as unknown as jest.Mock).mockClear();
+    (ResultSet as unknown as jest.Mock).mockImplementation(() => (
       <div data-test="mock-result-set" />
     ));
   });
@@ -182,7 +182,8 @@ describe('SqlEditor', () => {
     const editor = await findByTestId('react-ace');
     const sql = 'select *';
     const renderCount = (SqlEditorLeftBar as jest.Mock).mock.calls.length;
-    const renderCountForSouthPane = (ResultSet as jest.Mock).mock.calls.length;
+    const renderCountForSouthPane = (ResultSet as unknown 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 59bd0558a1..ce9eed9b9d 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: now(),
+        endDttm: action?.results?.query?.endDttm || now(),
         progress: 100,
         results: action.results,
         rows: action?.results?.query?.rows || 0,
@@ -674,7 +674,14 @@ export default function sqlLabReducer(state = {}, action) {
       if (!change) {
         newQueries = state.queries;
       }
-      return { ...state, queries: newQueries, queriesLastUpdate };
+      return {
+        ...state,
+        queries: newQueries,
+        queriesLastUpdate:
+          queriesLastUpdate > state.queriesLastUpdate
+            ? queriesLastUpdate
+            : Date.now(),
+      };
     },
     [actions.CLEAR_INACTIVE_QUERIES]() {
       const { queries } = state;
@@ -701,7 +708,11 @@ export default function sqlLabReducer(state = {}, action) {
             },
           ]),
       );
-      return { ...state, queries: cleanedQueries };
+      return {
+        ...state,
+        queries: cleanedQueries,
+        queriesLastUpdate: Date.now(),
+      };
     },
     [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 e1a234734b..5a70f10bb3 100644
--- a/superset-frontend/src/SqlLab/reducers/sqlLab.test.js
+++ b/superset-frontend/src/SqlLab/reducers/sqlLab.test.js
@@ -20,6 +20,7 @@ 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;
 
@@ -404,6 +405,7 @@ describe('sqlLabReducer', () => {
       };
     });
     it('updates queries that have already been completed', () => {
+      const current = Date.now();
       newState = sqlLabReducer(
         {
           ...newState,
@@ -418,9 +420,10 @@ describe('sqlLabReducer', () => {
             },
           },
         },
-        actions.clearInactiveQueries(Date.now()),
+        actions.clearInactiveQueries(QUERY_UPDATE_FREQ),
       );
       expect(newState.queries.abcd.state).toBe(QueryState.SUCCESS);
+      expect(newState.queriesLastUpdate).toBeGreaterThanOrEqual(current);
     });
   });
 });


(superset) 09/11: fix: support custom links in markdown (#26211)

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 c2612d8c2668a9a74441ba8135e1d8a1c668848a
Author: Ville Brofeldt <33...@users.noreply.github.com>
AuthorDate: Thu Dec 7 13:51:15 2023 -0800

    fix: support custom links in markdown (#26211)
    
    (cherry picked from commit d2adc858cbdf1242d96cf7cc0363e39afba88990)
---
 .../packages/superset-ui-core/src/components/SafeMarkdown.tsx            | 1 +
 1 file changed, 1 insertion(+)

diff --git a/superset-frontend/packages/superset-ui-core/src/components/SafeMarkdown.tsx b/superset-frontend/packages/superset-ui-core/src/components/SafeMarkdown.tsx
index b0826ce2ed..2b36802d4b 100644
--- a/superset-frontend/packages/superset-ui-core/src/components/SafeMarkdown.tsx
+++ b/superset-frontend/packages/superset-ui-core/src/components/SafeMarkdown.tsx
@@ -67,6 +67,7 @@ function SafeMarkdown({
       rehypePlugins={rehypePlugins}
       remarkPlugins={[remarkGfm]}
       skipHtml={false}
+      transformLinkUri={null}
     >
       {source}
     </ReactMarkdown>


(superset) 10/11: fix(chart-filter): Avoid column denormalization if not enabled (#26199)

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 b699df703047c13dd38e46a8ca64da53f1262714
Author: Vitor Avila <96...@users.noreply.github.com>
AuthorDate: Thu Dec 7 22:24:47 2023 -0300

    fix(chart-filter): Avoid column denormalization if not enabled (#26199)
    
    (cherry picked from commit 05d7060d838271ae46176040bcdd02b6fa359c72)
---
 superset/datasource/api.py |  5 ++++-
 superset/models/helpers.py | 17 +++++++++++------
 2 files changed, 15 insertions(+), 7 deletions(-)

diff --git a/superset/datasource/api.py b/superset/datasource/api.py
index 131d115755..6943d00bc7 100644
--- a/superset/datasource/api.py
+++ b/superset/datasource/api.py
@@ -115,9 +115,12 @@ class DatasourceRestApi(BaseSupersetApi):
             return self.response(403, message=ex.message)
 
         row_limit = apply_max_row_limit(app.config["FILTER_SELECT_ROW_LIMIT"])
+        denormalize_column = not datasource.normalize_columns
         try:
             payload = datasource.values_for_column(
-                column_name=column_name, limit=row_limit
+                column_name=column_name,
+                limit=row_limit,
+                denormalize_column=denormalize_column,
             )
             return self.response(200, result=payload)
         except KeyError:
diff --git a/superset/models/helpers.py b/superset/models/helpers.py
index df3dd93488..3e88bec44f 100644
--- a/superset/models/helpers.py
+++ b/superset/models/helpers.py
@@ -792,7 +792,7 @@ class ExploreMixin:  # pylint: disable=too-many-public-methods
         self,
         template_processor: Optional[  # pylint: disable=unused-argument
             BaseTemplateProcessor
-        ] = None,  # pylint: disable=unused-argument
+        ] = None,
     ) -> TextClause:
         return self.fetch_values_predicate
 
@@ -1340,14 +1340,19 @@ class ExploreMixin:  # pylint: disable=too-many-public-methods
             )
         return and_(*l)
 
-    def values_for_column(self, column_name: str, limit: int = 10000) -> list[Any]:
-        # always denormalize column name before querying for values
+    def values_for_column(
+        self, column_name: str, limit: int = 10000, denormalize_column: bool = False
+    ) -> list[Any]:
+        # denormalize column name before querying for values
+        # unless disabled in the dataset configuration
         db_dialect = self.database.get_dialect()
-        denormalized_col_name = self.database.db_engine_spec.denormalize_name(
-            db_dialect, column_name
+        column_name_ = (
+            self.database.db_engine_spec.denormalize_name(db_dialect, column_name)
+            if denormalize_column
+            else column_name
         )
         cols = {col.column_name: col for col in self.columns}
-        target_col = cols[denormalized_col_name]
+        target_col = cols[column_name_]
         tp = self.get_template_processor()
         tbl, cte = self.get_from_clause(tp)
 


(superset) 03/11: fix(init-job): Fix envFrom for init job in helm chart (#26157)

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 2104a9a8536d889cb7c14738899fd046b3f32efb
Author: Suma Goud B <55...@users.noreply.github.com>
AuthorDate: Wed Dec 6 16:40:43 2023 +0100

    fix(init-job): Fix envFrom for init job in helm chart (#26157)
    
    (cherry picked from commit 4d4b19e8bacc59a7257c46589d7151b3a4e6ee49)
---
 helm/superset/Chart.yaml              | 2 +-
 helm/superset/README.md               | 2 +-
 helm/superset/templates/init-job.yaml | 2 +-
 3 files changed, 3 insertions(+), 3 deletions(-)

diff --git a/helm/superset/Chart.yaml b/helm/superset/Chart.yaml
index 459392f8a1..cbca942569 100644
--- a/helm/superset/Chart.yaml
+++ b/helm/superset/Chart.yaml
@@ -29,7 +29,7 @@ maintainers:
   - name: craig-rueda
     email: craig@craigrueda.com
     url: https://github.com/craig-rueda
-version: 0.11.1
+version: 0.11.2
 dependencies:
   - name: postgresql
     version: 12.1.6
diff --git a/helm/superset/README.md b/helm/superset/README.md
index 886623e3f8..1eaf4928c1 100644
--- a/helm/superset/README.md
+++ b/helm/superset/README.md
@@ -23,7 +23,7 @@ NOTE: This file is generated by helm-docs: https://github.com/norwoodj/helm-docs
 
 # superset
 
-![Version: 0.11.1](https://img.shields.io/badge/Version-0.11.1-informational?style=flat-square)
+![Version: 0.11.2](https://img.shields.io/badge/Version-0.11.2-informational?style=flat-square)
 
 Apache Superset is a modern, enterprise-ready business intelligence web application
 
diff --git a/helm/superset/templates/init-job.yaml b/helm/superset/templates/init-job.yaml
index 5b39d20e10..43839c0d95 100644
--- a/helm/superset/templates/init-job.yaml
+++ b/helm/superset/templates/init-job.yaml
@@ -63,7 +63,7 @@ spec:
               name: {{ tpl .Values.envFromSecret . }}
           {{- range .Values.envFromSecrets }}
           - secretRef:
-              name: {{ tpl . $ }}
+              name: {{ tpl . $ | quote }}
           {{- end }}
         imagePullPolicy: {{ .Values.image.pullPolicy }}
         {{- if .Values.init.containerSecurityContext }}


(superset) 04/11: chore: Lower giveup log level for retried functions to warning (#26188)

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 5c24c580dd5561b3d8b7fbc15644ab41e3cad784
Author: Jack Fragassi <jf...@gmail.com>
AuthorDate: Wed Dec 6 16:33:19 2023 -0800

    chore: Lower giveup log level for retried functions to warning (#26188)
    
    (cherry picked from commit bf5b18ccb14cdf5fbc16037fb4db096e577799f2)
---
 superset/utils/retries.py | 3 +++
 1 file changed, 3 insertions(+)

diff --git a/superset/utils/retries.py b/superset/utils/retries.py
index 8a1e6b95ea..3af821362d 100644
--- a/superset/utils/retries.py
+++ b/superset/utils/retries.py
@@ -15,6 +15,7 @@
 # specific language governing permissions and limitations
 # under the License.
 
+import logging
 from collections.abc import Generator
 from typing import Any, Callable, Optional
 
@@ -26,6 +27,7 @@ def retry_call(
     *args: Any,
     strategy: Callable[..., Generator[int, None, None]] = backoff.constant,
     exception: type[Exception] = Exception,
+    giveup_log_level: int = logging.WARNING,
     fargs: Optional[list[Any]] = None,
     fkwargs: Optional[dict[str, Any]] = None,
     **kwargs: Any
@@ -33,6 +35,7 @@ def retry_call(
     """
     Retry a given call.
     """
+    kwargs["giveup_log_level"] = giveup_log_level
     decorated = backoff.on_exception(strategy, exception, *args, **kwargs)(func)
     fargs = fargs or []
     fkwargs = fkwargs or {}


(superset) 11/11: fix(plugin-chart-echarts): support truncated numeric x-axis (#26215)

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 d0961d0ed8db1b607f981b3c19f9779eb9ce99ff
Author: Ville Brofeldt <33...@users.noreply.github.com>
AuthorDate: Fri Dec 8 05:40:09 2023 -0800

    fix(plugin-chart-echarts): support truncated numeric x-axis (#26215)
    
    Co-authored-by: Michael S. Molina <mi...@gmail.com>
    (cherry picked from commit 07e5fe8a66fcce6baf1974de9ff3aaab4ad30884)
---
 .../plugin-chart-echarts/src/Bubble/constants.ts   |  1 +
 .../src/Bubble/controlPanel.tsx                    |  4 ++-
 .../src/Bubble/transformProps.ts                   | 17 +++++++-----
 .../src/MixedTimeseries/transformProps.ts          |  6 ++---
 .../src/Timeseries/Area/controlPanel.tsx           |  4 +++
 .../src/Timeseries/Regular/Bar/controlPanel.tsx    |  4 +++
 .../src/Timeseries/Regular/Line/controlPanel.tsx   |  4 +++
 .../Timeseries/Regular/Scatter/controlPanel.tsx    |  4 +++
 .../Timeseries/Regular/SmoothLine/controlPanel.tsx |  4 +++
 .../src/Timeseries/Step/controlPanel.tsx           |  4 +++
 .../src/Timeseries/constants.ts                    |  1 +
 .../src/Timeseries/transformProps.ts               | 28 ++++++++++++-------
 .../plugin-chart-echarts/src/Timeseries/types.ts   |  2 ++
 .../plugins/plugin-chart-echarts/src/controls.tsx  | 31 ++++++++++++++++++++++
 .../plugin-chart-echarts/src/utils/controls.ts     |  2 +-
 .../plugin-chart-echarts/src/utils/series.ts       | 14 ++++++++++
 .../test/Bubble/transformProps.test.ts             |  1 +
 .../test/utils/controls.test.ts                    | 22 +++++++--------
 .../plugin-chart-echarts/test/utils/series.test.ts | 28 +++++++++++++++++++
 19 files changed, 150 insertions(+), 31 deletions(-)

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 0f9bc0f305..9c157d33ac 100644
--- a/superset-frontend/plugins/plugin-chart-echarts/src/Bubble/constants.ts
+++ b/superset-frontend/plugins/plugin-chart-echarts/src/Bubble/constants.ts
@@ -26,6 +26,7 @@ export const DEFAULT_FORM_DATA: Partial<EchartsBubbleFormData> = {
   logYAxis: false,
   xAxisTitleMargin: 30,
   yAxisTitleMargin: 30,
+  truncateXAxis: false,
   truncateYAxis: false,
   yAxisBounds: [null, null],
   xAxisLabelRotation: 0,
diff --git a/superset-frontend/plugins/plugin-chart-echarts/src/Bubble/controlPanel.tsx b/superset-frontend/plugins/plugin-chart-echarts/src/Bubble/controlPanel.tsx
index 50f42eda23..532888b485 100644
--- a/superset-frontend/plugins/plugin-chart-echarts/src/Bubble/controlPanel.tsx
+++ b/superset-frontend/plugins/plugin-chart-echarts/src/Bubble/controlPanel.tsx
@@ -26,7 +26,7 @@ import {
 } from '@superset-ui/chart-controls';
 
 import { DEFAULT_FORM_DATA } from './constants';
-import { legendSection } from '../controls';
+import { legendSection, truncateXAxis, xAxisBounds } from '../controls';
 
 const { logAxis, truncateYAxis, yAxisBounds, xAxisLabelRotation, opacity } =
   DEFAULT_FORM_DATA;
@@ -247,6 +247,8 @@ const config: ControlPanelConfig = {
             },
           },
         ],
+        [truncateXAxis],
+        [xAxisBounds],
         [
           {
             name: 'truncateYAxis',
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 ce53fdf266..01d9ed3c53 100644
--- a/superset-frontend/plugins/plugin-chart-echarts/src/Bubble/transformProps.ts
+++ b/superset-frontend/plugins/plugin-chart-echarts/src/Bubble/transformProps.ts
@@ -28,9 +28,9 @@ import {
 import { EchartsBubbleChartProps, EchartsBubbleFormData } from './types';
 import { DEFAULT_FORM_DATA, MINIMUM_BUBBLE_SIZE } from './constants';
 import { defaultGrid } from '../defaults';
-import { getLegendProps } from '../utils/series';
+import { getLegendProps, getMinAndMaxFromBounds } from '../utils/series';
 import { Refs } from '../types';
-import { parseYAxisBound } from '../utils/controls';
+import { parseAxisBound } from '../utils/controls';
 import { getDefaultTooltip } from '../utils/tooltip';
 import { getPadding } from '../Timeseries/transformers';
 import { convertInteger } from '../utils/convertInteger';
@@ -84,6 +84,7 @@ export default function transformProps(chartProps: EchartsBubbleChartProps) {
     series: bubbleSeries,
     xAxisLabel: bubbleXAxisTitle,
     yAxisLabel: bubbleYAxisTitle,
+    xAxisBounds,
     xAxisFormat,
     yAxisFormat,
     yAxisBounds,
@@ -91,6 +92,7 @@ export default function transformProps(chartProps: EchartsBubbleChartProps) {
     logYAxis,
     xAxisTitleMargin,
     yAxisTitleMargin,
+    truncateXAxis,
     truncateYAxis,
     xAxisLabelRotation,
     yAxisLabelRotation,
@@ -141,7 +143,8 @@ export default function transformProps(chartProps: EchartsBubbleChartProps) {
   const yAxisFormatter = getNumberFormatter(yAxisFormat);
   const tooltipSizeFormatter = getNumberFormatter(tooltipSizeFormat);
 
-  const [min, max] = yAxisBounds.map(parseYAxisBound);
+  const [xAxisMin, xAxisMax] = xAxisBounds.map(parseAxisBound);
+  const [yAxisMin, yAxisMax] = yAxisBounds.map(parseAxisBound);
 
   const padding = getPadding(
     showLegend,
@@ -155,6 +158,7 @@ export default function transformProps(chartProps: EchartsBubbleChartProps) {
     convertInteger(xAxisTitleMargin),
   );
 
+  const xAxisType = logXAxis ? AxisType.log : AxisType.value;
   const echartOptions: EChartsCoreOption = {
     series,
     xAxis: {
@@ -172,7 +176,8 @@ export default function transformProps(chartProps: EchartsBubbleChartProps) {
         fontWight: 'bolder',
       },
       nameGap: convertInteger(xAxisTitleMargin),
-      type: logXAxis ? AxisType.log : AxisType.value,
+      type: xAxisType,
+      ...getMinAndMaxFromBounds(xAxisType, truncateXAxis, xAxisMin, xAxisMax),
     },
     yAxis: {
       axisLabel: { formatter: yAxisFormatter },
@@ -189,8 +194,8 @@ export default function transformProps(chartProps: EchartsBubbleChartProps) {
         fontWight: 'bolder',
       },
       nameGap: convertInteger(yAxisTitleMargin),
-      min,
-      max,
+      min: yAxisMin,
+      max: yAxisMax,
       type: logYAxis ? AxisType.log : AxisType.value,
     },
     legend: {
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 47411e2477..8bc01582af 100644
--- a/superset-frontend/plugins/plugin-chart-echarts/src/MixedTimeseries/transformProps.ts
+++ b/superset-frontend/plugins/plugin-chart-echarts/src/MixedTimeseries/transformProps.ts
@@ -53,7 +53,7 @@ import {
   ForecastSeriesEnum,
   Refs,
 } from '../types';
-import { parseYAxisBound } from '../utils/controls';
+import { parseAxisBound } from '../utils/controls';
 import {
   getOverMaxHiddenFormatter,
   dedupSeries,
@@ -345,9 +345,9 @@ export default function transformProps(
     });
 
   // yAxisBounds need to be parsed to replace incompatible values with undefined
-  let [min, max] = (yAxisBounds || []).map(parseYAxisBound);
+  let [min, max] = (yAxisBounds || []).map(parseAxisBound);
   let [minSecondary, maxSecondary] = (yAxisBoundsSecondary || []).map(
-    parseYAxisBound,
+    parseAxisBound,
   );
 
   const array = ensureIsArray(chartProps.rawFormData?.time_compare);
diff --git a/superset-frontend/plugins/plugin-chart-echarts/src/Timeseries/Area/controlPanel.tsx b/superset-frontend/plugins/plugin-chart-echarts/src/Timeseries/Area/controlPanel.tsx
index 5a5975c66b..018038772c 100644
--- a/superset-frontend/plugins/plugin-chart-echarts/src/Timeseries/Area/controlPanel.tsx
+++ b/superset-frontend/plugins/plugin-chart-echarts/src/Timeseries/Area/controlPanel.tsx
@@ -37,6 +37,8 @@ import {
   richTooltipSection,
   seriesOrderSection,
   percentageThresholdControl,
+  truncateXAxis,
+  xAxisBounds,
 } from '../../controls';
 import { AreaChartStackControlOptions } from '../../constants';
 
@@ -241,6 +243,8 @@ const config: ControlPanelConfig = {
             },
           },
         ],
+        [truncateXAxis],
+        [xAxisBounds],
         [
           {
             name: 'truncateYAxis',
diff --git a/superset-frontend/plugins/plugin-chart-echarts/src/Timeseries/Regular/Bar/controlPanel.tsx b/superset-frontend/plugins/plugin-chart-echarts/src/Timeseries/Regular/Bar/controlPanel.tsx
index af482da1b4..c3002a2498 100644
--- a/superset-frontend/plugins/plugin-chart-echarts/src/Timeseries/Regular/Bar/controlPanel.tsx
+++ b/superset-frontend/plugins/plugin-chart-echarts/src/Timeseries/Regular/Bar/controlPanel.tsx
@@ -35,6 +35,8 @@ import {
   richTooltipSection,
   seriesOrderSection,
   showValueSection,
+  truncateXAxis,
+  xAxisBounds,
 } from '../../../controls';
 
 import { OrientationType } from '../../types';
@@ -224,6 +226,8 @@ function createAxisControl(axis: 'x' | 'y'): ControlSetRow[] {
         },
       },
     ],
+    [truncateXAxis],
+    [xAxisBounds],
     [
       {
         name: 'truncateYAxis',
diff --git a/superset-frontend/plugins/plugin-chart-echarts/src/Timeseries/Regular/Line/controlPanel.tsx b/superset-frontend/plugins/plugin-chart-echarts/src/Timeseries/Regular/Line/controlPanel.tsx
index 124ab1e935..5c5f7a0ab1 100644
--- a/superset-frontend/plugins/plugin-chart-echarts/src/Timeseries/Regular/Line/controlPanel.tsx
+++ b/superset-frontend/plugins/plugin-chart-echarts/src/Timeseries/Regular/Line/controlPanel.tsx
@@ -38,6 +38,8 @@ import {
   richTooltipSection,
   seriesOrderSection,
   showValueSection,
+  truncateXAxis,
+  xAxisBounds,
 } from '../../../controls';
 
 const {
@@ -229,6 +231,8 @@ const config: ControlPanelConfig = {
             },
           },
         ],
+        [truncateXAxis],
+        [xAxisBounds],
         [
           {
             name: 'truncateYAxis',
diff --git a/superset-frontend/plugins/plugin-chart-echarts/src/Timeseries/Regular/Scatter/controlPanel.tsx b/superset-frontend/plugins/plugin-chart-echarts/src/Timeseries/Regular/Scatter/controlPanel.tsx
index bc813127ca..6701647694 100644
--- a/superset-frontend/plugins/plugin-chart-echarts/src/Timeseries/Regular/Scatter/controlPanel.tsx
+++ b/superset-frontend/plugins/plugin-chart-echarts/src/Timeseries/Regular/Scatter/controlPanel.tsx
@@ -37,6 +37,8 @@ import {
   richTooltipSection,
   seriesOrderSection,
   showValueSection,
+  truncateXAxis,
+  xAxisBounds,
 } from '../../../controls';
 
 const {
@@ -173,6 +175,8 @@ const config: ControlPanelConfig = {
             },
           },
         ],
+        [truncateXAxis],
+        [xAxisBounds],
         [
           {
             name: 'truncateYAxis',
diff --git a/superset-frontend/plugins/plugin-chart-echarts/src/Timeseries/Regular/SmoothLine/controlPanel.tsx b/superset-frontend/plugins/plugin-chart-echarts/src/Timeseries/Regular/SmoothLine/controlPanel.tsx
index 2a8fbfb0af..0c6623f216 100644
--- a/superset-frontend/plugins/plugin-chart-echarts/src/Timeseries/Regular/SmoothLine/controlPanel.tsx
+++ b/superset-frontend/plugins/plugin-chart-echarts/src/Timeseries/Regular/SmoothLine/controlPanel.tsx
@@ -37,6 +37,8 @@ import {
   richTooltipSection,
   seriesOrderSection,
   showValueSectionWithoutStack,
+  truncateXAxis,
+  xAxisBounds,
 } from '../../../controls';
 
 const {
@@ -173,6 +175,8 @@ const config: ControlPanelConfig = {
             },
           },
         ],
+        [truncateXAxis],
+        [xAxisBounds],
         [
           {
             name: 'truncateYAxis',
diff --git a/superset-frontend/plugins/plugin-chart-echarts/src/Timeseries/Step/controlPanel.tsx b/superset-frontend/plugins/plugin-chart-echarts/src/Timeseries/Step/controlPanel.tsx
index 9333cb48a1..021c306d05 100644
--- a/superset-frontend/plugins/plugin-chart-echarts/src/Timeseries/Step/controlPanel.tsx
+++ b/superset-frontend/plugins/plugin-chart-echarts/src/Timeseries/Step/controlPanel.tsx
@@ -35,6 +35,8 @@ import {
   richTooltipSection,
   seriesOrderSection,
   showValueSection,
+  truncateXAxis,
+  xAxisBounds,
 } from '../../controls';
 
 const {
@@ -223,6 +225,8 @@ const config: ControlPanelConfig = {
             },
           },
         ],
+        [truncateXAxis],
+        [xAxisBounds],
         [
           {
             name: 'truncateYAxis',
diff --git a/superset-frontend/plugins/plugin-chart-echarts/src/Timeseries/constants.ts b/superset-frontend/plugins/plugin-chart-echarts/src/Timeseries/constants.ts
index 17629c0996..a95a14077d 100644
--- a/superset-frontend/plugins/plugin-chart-echarts/src/Timeseries/constants.ts
+++ b/superset-frontend/plugins/plugin-chart-echarts/src/Timeseries/constants.ts
@@ -57,6 +57,7 @@ export const DEFAULT_FORM_DATA: EchartsTimeseriesFormData = {
   seriesType: EchartsTimeseriesSeriesType.Line,
   stack: false,
   tooltipTimeFormat: 'smart_date',
+  truncateXAxis: true,
   truncateYAxis: false,
   yAxisBounds: [null, null],
   zoomable: false,
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 c85dc8db00..e42ac183b6 100644
--- a/superset-frontend/plugins/plugin-chart-echarts/src/Timeseries/transformProps.ts
+++ b/superset-frontend/plugins/plugin-chart-echarts/src/Timeseries/transformProps.ts
@@ -54,7 +54,7 @@ import {
 } from './types';
 import { DEFAULT_FORM_DATA } from './constants';
 import { ForecastSeriesEnum, ForecastValue, Refs } from '../types';
-import { parseYAxisBound } from '../utils/controls';
+import { parseAxisBound } from '../utils/controls';
 import {
   calculateLowerLogTick,
   dedupSeries,
@@ -64,6 +64,7 @@ import {
   getAxisType,
   getColtypesMapping,
   getLegendProps,
+  getMinAndMaxFromBounds,
 } from '../utils/series';
 import {
   extractAnnotationLabels,
@@ -161,8 +162,10 @@ export default function transformProps(
     stack,
     tooltipTimeFormat,
     tooltipSortByMetric,
+    truncateXAxis,
     truncateYAxis,
     xAxis: xAxisOrig,
+    xAxisBounds,
     xAxisLabelRotation,
     xAxisSortSeries,
     xAxisSortSeriesAscending,
@@ -388,15 +391,20 @@ export default function transformProps(
       }
     });
 
-  // yAxisBounds need to be parsed to replace incompatible values with undefined
-  let [min, max] = (yAxisBounds || []).map(parseYAxisBound);
+  // axis bounds need to be parsed to replace incompatible values with undefined
+  const [xAxisMin, xAxisMax] = (xAxisBounds || []).map(parseAxisBound);
+  let [yAxisMin, yAxisMax] = (yAxisBounds || []).map(parseAxisBound);
 
   // default to 0-100% range when doing row-level contribution chart
   if ((contributionMode === 'row' || isAreaExpand) && stack) {
-    if (min === undefined) min = 0;
-    if (max === undefined) max = 1;
-  } else if (logAxis && min === undefined && minPositiveValue !== undefined) {
-    min = calculateLowerLogTick(minPositiveValue);
+    if (yAxisMin === undefined) yAxisMin = 0;
+    if (yAxisMax === undefined) yAxisMax = 1;
+  } else if (
+    logAxis &&
+    yAxisMin === undefined &&
+    minPositiveValue !== undefined
+  ) {
+    yAxisMin = calculateLowerLogTick(minPositiveValue);
   }
 
   const tooltipFormatter =
@@ -452,12 +460,14 @@ export default function transformProps(
       xAxisType === AxisType.time && timeGrainSqla
         ? TIMEGRAIN_TO_TIMESTAMP[timeGrainSqla]
         : 0,
+    ...getMinAndMaxFromBounds(xAxisType, truncateXAxis, xAxisMin, xAxisMax),
   };
+
   let yAxis: any = {
     ...defaultYAxis,
     type: logAxis ? AxisType.log : AxisType.value,
-    min,
-    max,
+    min: yAxisMin,
+    max: yAxisMax,
     minorTick: { show: true },
     minorSplitLine: { show: minorSplitLine },
     axisLabel: {
diff --git a/superset-frontend/plugins/plugin-chart-echarts/src/Timeseries/types.ts b/superset-frontend/plugins/plugin-chart-echarts/src/Timeseries/types.ts
index 1873086d99..65da981e49 100644
--- a/superset-frontend/plugins/plugin-chart-echarts/src/Timeseries/types.ts
+++ b/superset-frontend/plugins/plugin-chart-echarts/src/Timeseries/types.ts
@@ -75,10 +75,12 @@ export type EchartsTimeseriesFormData = QueryFormData & {
   stack: StackType;
   timeCompare?: string[];
   tooltipTimeFormat?: string;
+  truncateXAxis: boolean;
   truncateYAxis: boolean;
   yAxisFormat?: string;
   xAxisTimeFormat?: string;
   timeGrainSqla?: TimeGranularity;
+  xAxisBounds: [number | undefined | null, number | undefined | null];
   yAxisBounds: [number | undefined | null, number | undefined | null];
   zoomable: boolean;
   richTooltip: boolean;
diff --git a/superset-frontend/plugins/plugin-chart-echarts/src/controls.tsx b/superset-frontend/plugins/plugin-chart-echarts/src/controls.tsx
index 8f311e47e5..093617446a 100644
--- a/superset-frontend/plugins/plugin-chart-echarts/src/controls.tsx
+++ b/superset-frontend/plugins/plugin-chart-echarts/src/controls.tsx
@@ -248,3 +248,34 @@ export const seriesOrderSection: ControlSetRow[] = [
   [sortSeriesType],
   [sortSeriesAscending],
 ];
+
+export const truncateXAxis: ControlSetItem = {
+  name: 'truncateXAxis',
+  config: {
+    type: 'CheckboxControl',
+    label: t('Truncate X Axis'),
+    default: DEFAULT_FORM_DATA.truncateXAxis,
+    renderTrigger: true,
+    description: t(
+      'Truncate X Axis. Can be overridden by specifying a min or max bound. Only applicable for numercal X axis.',
+    ),
+  },
+};
+
+export const xAxisBounds: ControlSetItem = {
+  name: 'xAxisBounds',
+  config: {
+    type: 'BoundsControl',
+    label: t('X Axis Bounds'),
+    renderTrigger: true,
+    default: DEFAULT_FORM_DATA.xAxisBounds,
+    description: t(
+      'Bounds for numerical X axis. Not applicable for temporal or categorical axes. ' +
+        'When left empty, the bounds are dynamically defined based on the min/max of the data. ' +
+        "Note that this feature will only expand the axis range. It won't " +
+        "narrow the data's extent.",
+    ),
+    visibility: ({ controls }: ControlPanelsContainerProps) =>
+      Boolean(controls?.truncateXAxis?.value),
+  },
+};
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 27f8fb1447..67a5414112 100644
--- a/superset-frontend/plugins/plugin-chart-echarts/src/utils/controls.ts
+++ b/superset-frontend/plugins/plugin-chart-echarts/src/utils/controls.ts
@@ -20,7 +20,7 @@
 import { validateNumber } from '@superset-ui/core';
 
 // eslint-disable-next-line import/prefer-default-export
-export function parseYAxisBound(
+export function parseAxisBound(
   bound?: string | number | null,
 ): number | undefined {
   if (bound === undefined || bound === null || Number.isNaN(Number(bound))) {
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 bd4e329d0b..aa353f66d1 100644
--- a/superset-frontend/plugins/plugin-chart-echarts/src/utils/series.ts
+++ b/superset-frontend/plugins/plugin-chart-echarts/src/utils/series.ts
@@ -543,3 +543,17 @@ export function calculateLowerLogTick(minPositiveValue: number) {
   const logBase10 = Math.floor(Math.log10(minPositiveValue));
   return Math.pow(10, logBase10);
 }
+
+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,
+      }
+    : {};
+}
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 2bb4ae0fc6..1a92a43257 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
@@ -48,6 +48,7 @@ describe('Bubble transformProps', () => {
       expressionType: 'simple',
       label: 'SUM(sales)',
     },
+    xAxisBounds: [null, null],
     yAxisBounds: [null, null],
   };
   const chartProps = new ChartProps({
diff --git a/superset-frontend/plugins/plugin-chart-echarts/test/utils/controls.test.ts b/superset-frontend/plugins/plugin-chart-echarts/test/utils/controls.test.ts
index 60ced57739..cb0faac595 100644
--- a/superset-frontend/plugins/plugin-chart-echarts/test/utils/controls.test.ts
+++ b/superset-frontend/plugins/plugin-chart-echarts/test/utils/controls.test.ts
@@ -16,22 +16,22 @@
  * specific language governing permissions and limitations
  * under the License.
  */
-import { parseYAxisBound } from '../../src/utils/controls';
+import { parseAxisBound } from '../../src/utils/controls';
 
 describe('parseYAxisBound', () => {
   it('should return undefined for invalid values', () => {
-    expect(parseYAxisBound(null)).toBeUndefined();
-    expect(parseYAxisBound(undefined)).toBeUndefined();
-    expect(parseYAxisBound(NaN)).toBeUndefined();
-    expect(parseYAxisBound('abc')).toBeUndefined();
+    expect(parseAxisBound(null)).toBeUndefined();
+    expect(parseAxisBound(undefined)).toBeUndefined();
+    expect(parseAxisBound(NaN)).toBeUndefined();
+    expect(parseAxisBound('abc')).toBeUndefined();
   });
 
   it('should return numeric value for valid values', () => {
-    expect(parseYAxisBound(0)).toEqual(0);
-    expect(parseYAxisBound('0')).toEqual(0);
-    expect(parseYAxisBound(1)).toEqual(1);
-    expect(parseYAxisBound('1')).toEqual(1);
-    expect(parseYAxisBound(10.1)).toEqual(10.1);
-    expect(parseYAxisBound('10.1')).toEqual(10.1);
+    expect(parseAxisBound(0)).toEqual(0);
+    expect(parseAxisBound('0')).toEqual(0);
+    expect(parseAxisBound(1)).toEqual(1);
+    expect(parseAxisBound('1')).toEqual(1);
+    expect(parseAxisBound(10.1)).toEqual(10.1);
+    expect(parseAxisBound('10.1')).toEqual(10.1);
   });
 });
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 927ee49e8c..b445dceabb 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
@@ -36,6 +36,7 @@ import {
   getChartPadding,
   getLegendProps,
   getOverMaxHiddenFormatter,
+  getMinAndMaxFromBounds,
   sanitizeHtml,
   sortAndFilterSeries,
   sortRows,
@@ -879,3 +880,30 @@ test('getAxisType', () => {
   expect(getAxisType(GenericDataType.BOOLEAN)).toEqual(AxisType.category);
   expect(getAxisType(GenericDataType.STRING)).toEqual(AxisType.category);
 });
+
+test('getMinAndMaxFromBounds returns empty object when not truncating', () => {
+  expect(getMinAndMaxFromBounds(AxisType.value, false, 10, 100)).toEqual({});
+});
+
+test('getMinAndMaxFromBounds returns automatic bounds when truncating', () => {
+  expect(
+    getMinAndMaxFromBounds(AxisType.value, true, undefined, undefined),
+  ).toEqual({
+    min: 'dataMin',
+    max: 'dataMax',
+  });
+});
+
+test('getMinAndMaxFromBounds returns automatic upper bound when truncating', () => {
+  expect(getMinAndMaxFromBounds(AxisType.value, true, 10, undefined)).toEqual({
+    min: 10,
+    max: 'dataMax',
+  });
+});
+
+test('getMinAndMaxFromBounds returns automatic lower bound when truncating', () => {
+  expect(getMinAndMaxFromBounds(AxisType.value, true, undefined, 100)).toEqual({
+    min: 'dataMin',
+    max: 100,
+  });
+});


(superset) 05/11: fix: Includes 90° x-axis label rotation (#26207)

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 fb50819fcd363420c33e5f7441462ab0853f96e2
Author: Michael S. Molina <70...@users.noreply.github.com>
AuthorDate: Thu Dec 7 13:08:54 2023 -0300

    fix: Includes 90° x-axis label rotation (#26207)
    
    (cherry picked from commit 39c6488463ab81417223a2e1b171c769b86306cf)
---
 .../plugins/plugin-chart-echarts/src/Bubble/controlPanel.tsx             | 1 +
 .../plugins/plugin-chart-echarts/src/MixedTimeseries/controlPanel.tsx    | 1 +
 .../plugins/plugin-chart-echarts/src/Timeseries/Area/controlPanel.tsx    | 1 +
 .../plugin-chart-echarts/src/Timeseries/Regular/Bar/controlPanel.tsx     | 1 +
 .../plugin-chart-echarts/src/Timeseries/Regular/Line/controlPanel.tsx    | 1 +
 .../plugin-chart-echarts/src/Timeseries/Regular/Scatter/controlPanel.tsx | 1 +
 .../src/Timeseries/Regular/SmoothLine/controlPanel.tsx                   | 1 +
 .../plugins/plugin-chart-echarts/src/Timeseries/Step/controlPanel.tsx    | 1 +
 8 files changed, 8 insertions(+)

diff --git a/superset-frontend/plugins/plugin-chart-echarts/src/Bubble/controlPanel.tsx b/superset-frontend/plugins/plugin-chart-echarts/src/Bubble/controlPanel.tsx
index 53fba5de2b..50f42eda23 100644
--- a/superset-frontend/plugins/plugin-chart-echarts/src/Bubble/controlPanel.tsx
+++ b/superset-frontend/plugins/plugin-chart-echarts/src/Bubble/controlPanel.tsx
@@ -138,6 +138,7 @@ const config: ControlPanelConfig = {
               choices: [
                 [0, '0°'],
                 [45, '45°'],
+                [90, '90°'],
               ],
               default: xAxisLabelRotation,
               renderTrigger: true,
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 c9f9027a3e..ec2443bb60 100644
--- a/superset-frontend/plugins/plugin-chart-echarts/src/MixedTimeseries/controlPanel.tsx
+++ b/superset-frontend/plugins/plugin-chart-echarts/src/MixedTimeseries/controlPanel.tsx
@@ -325,6 +325,7 @@ const config: ControlPanelConfig = {
               choices: [
                 [0, '0°'],
                 [45, '45°'],
+                [90, '90°'],
               ],
               default: xAxisLabelRotation,
               renderTrigger: true,
diff --git a/superset-frontend/plugins/plugin-chart-echarts/src/Timeseries/Area/controlPanel.tsx b/superset-frontend/plugins/plugin-chart-echarts/src/Timeseries/Area/controlPanel.tsx
index 8515139548..5a5975c66b 100644
--- a/superset-frontend/plugins/plugin-chart-echarts/src/Timeseries/Area/controlPanel.tsx
+++ b/superset-frontend/plugins/plugin-chart-echarts/src/Timeseries/Area/controlPanel.tsx
@@ -202,6 +202,7 @@ const config: ControlPanelConfig = {
               choices: [
                 [0, '0°'],
                 [45, '45°'],
+                [90, '90°'],
               ],
               default: xAxisLabelRotation,
               renderTrigger: true,
diff --git a/superset-frontend/plugins/plugin-chart-echarts/src/Timeseries/Regular/Bar/controlPanel.tsx b/superset-frontend/plugins/plugin-chart-echarts/src/Timeseries/Regular/Bar/controlPanel.tsx
index 47fe550ad7..af482da1b4 100644
--- a/superset-frontend/plugins/plugin-chart-echarts/src/Timeseries/Regular/Bar/controlPanel.tsx
+++ b/superset-frontend/plugins/plugin-chart-echarts/src/Timeseries/Regular/Bar/controlPanel.tsx
@@ -172,6 +172,7 @@ function createAxisControl(axis: 'x' | 'y'): ControlSetRow[] {
           choices: [
             [0, '0°'],
             [45, '45°'],
+            [90, '90°'],
           ],
           default: xAxisLabelRotation,
           renderTrigger: true,
diff --git a/superset-frontend/plugins/plugin-chart-echarts/src/Timeseries/Regular/Line/controlPanel.tsx b/superset-frontend/plugins/plugin-chart-echarts/src/Timeseries/Regular/Line/controlPanel.tsx
index 637a5fbc57..124ab1e935 100644
--- a/superset-frontend/plugins/plugin-chart-echarts/src/Timeseries/Regular/Line/controlPanel.tsx
+++ b/superset-frontend/plugins/plugin-chart-echarts/src/Timeseries/Regular/Line/controlPanel.tsx
@@ -190,6 +190,7 @@ const config: ControlPanelConfig = {
               choices: [
                 [0, '0°'],
                 [45, '45°'],
+                [90, '90°'],
               ],
               default: xAxisLabelRotation,
               renderTrigger: true,
diff --git a/superset-frontend/plugins/plugin-chart-echarts/src/Timeseries/Regular/Scatter/controlPanel.tsx b/superset-frontend/plugins/plugin-chart-echarts/src/Timeseries/Regular/Scatter/controlPanel.tsx
index ffcee71792..bc813127ca 100644
--- a/superset-frontend/plugins/plugin-chart-echarts/src/Timeseries/Regular/Scatter/controlPanel.tsx
+++ b/superset-frontend/plugins/plugin-chart-echarts/src/Timeseries/Regular/Scatter/controlPanel.tsx
@@ -133,6 +133,7 @@ const config: ControlPanelConfig = {
               choices: [
                 [0, '0°'],
                 [45, '45°'],
+                [90, '90°'],
               ],
               default: xAxisLabelRotation,
               renderTrigger: true,
diff --git a/superset-frontend/plugins/plugin-chart-echarts/src/Timeseries/Regular/SmoothLine/controlPanel.tsx b/superset-frontend/plugins/plugin-chart-echarts/src/Timeseries/Regular/SmoothLine/controlPanel.tsx
index cb7164e0ab..2a8fbfb0af 100644
--- a/superset-frontend/plugins/plugin-chart-echarts/src/Timeseries/Regular/SmoothLine/controlPanel.tsx
+++ b/superset-frontend/plugins/plugin-chart-echarts/src/Timeseries/Regular/SmoothLine/controlPanel.tsx
@@ -132,6 +132,7 @@ const config: ControlPanelConfig = {
               choices: [
                 [0, '0°'],
                 [45, '45°'],
+                [90, '90°'],
               ],
               default: xAxisLabelRotation,
               renderTrigger: true,
diff --git a/superset-frontend/plugins/plugin-chart-echarts/src/Timeseries/Step/controlPanel.tsx b/superset-frontend/plugins/plugin-chart-echarts/src/Timeseries/Step/controlPanel.tsx
index 1921e698c2..9333cb48a1 100644
--- a/superset-frontend/plugins/plugin-chart-echarts/src/Timeseries/Step/controlPanel.tsx
+++ b/superset-frontend/plugins/plugin-chart-echarts/src/Timeseries/Step/controlPanel.tsx
@@ -184,6 +184,7 @@ const config: ControlPanelConfig = {
               choices: [
                 [0, '0°'],
                 [45, '45°'],
+                [90, '90°'],
               ],
               default: xAxisLabelRotation,
               renderTrigger: true,


(superset) 01/11: fix(menu): Styling active menu in SPA navigation (#25533)

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 96c0497fa931f86654ff157582d9aba143ea5ae9
Author: JUST.in DO IT <ju...@airbnb.com>
AuthorDate: Mon Dec 4 13:03:29 2023 -0800

    fix(menu): Styling active menu in SPA navigation (#25533)
    
    (cherry picked from commit 86304ab17199ef45eef1f471defd025e0b225cfc)
---
 superset-frontend/src/features/home/Menu.tsx | 37 +++++++++++++++++++++++++---
 1 file changed, 33 insertions(+), 4 deletions(-)

diff --git a/superset-frontend/src/features/home/Menu.tsx b/superset-frontend/src/features/home/Menu.tsx
index 56a2fd611e..67b72fc515 100644
--- a/superset-frontend/src/features/home/Menu.tsx
+++ b/superset-frontend/src/features/home/Menu.tsx
@@ -24,7 +24,7 @@ import { getUrlParam } from 'src/utils/urlUtils';
 import { Row, Col, Grid } from 'src/components';
 import { MainNav as DropdownMenu, MenuMode } from 'src/components/Menu';
 import { Tooltip } from 'src/components/Tooltip';
-import { Link, useLocation } from 'react-router-dom';
+import { NavLink, useLocation } from 'react-router-dom';
 import { GenericLink } from 'src/components/GenericLink/GenericLink';
 import Icons from 'src/components/Icons';
 import { useUiConfig } from 'src/components/UiConfigContext';
@@ -154,6 +154,29 @@ const globalStyles = (theme: SupersetTheme) => css`
       margin-left: ${theme.gridUnit * 1.75}px;
     }
   }
+  .ant-menu-item-selected {
+    background-color: transparent;
+    &:not(.ant-menu-item-active) {
+      color: inherit;
+      border-bottom-color: transparent;
+      & > a {
+        color: inherit;
+      }
+    }
+  }
+  .ant-menu-horizontal > .ant-menu-item:has(> .is-active) {
+    color: ${theme.colors.primary.base};
+    border-bottom-color: ${theme.colors.primary.base};
+    & > a {
+      color: ${theme.colors.primary.base};
+    }
+  }
+  .ant-menu-vertical > .ant-menu-item:has(> .is-active) {
+    background-color: ${theme.colors.primary.light5};
+    & > a {
+      color: ${theme.colors.primary.base};
+    }
+  }
 `;
 const { SubMenu } = DropdownMenu;
 
@@ -226,9 +249,9 @@ export function Menu({
     if (url && isFrontendRoute) {
       return (
         <DropdownMenu.Item key={label} role="presentation">
-          <Link role="button" to={url}>
+          <NavLink role="button" to={url} activeClassName="is-active">
             {label}
-          </Link>
+          </NavLink>
         </DropdownMenu.Item>
       );
     }
@@ -253,7 +276,13 @@ export function Menu({
             return (
               <DropdownMenu.Item key={`${child.label}`}>
                 {child.isFrontendRoute ? (
-                  <Link to={child.url || ''}>{child.label}</Link>
+                  <NavLink
+                    to={child.url || ''}
+                    exact
+                    activeClassName="is-active"
+                  >
+                    {child.label}
+                  </NavLink>
                 ) : (
                   <a href={child.url}>{child.label}</a>
                 )}


(superset) 02/11: fix(embedded): Hide sensitive payload data from guest users (#25878)

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 0925d75dfac51964a03977a126990e385f1714f1
Author: Jack Fragassi <jf...@gmail.com>
AuthorDate: Mon Dec 4 14:52:59 2023 -0800

    fix(embedded): Hide sensitive payload data from guest users (#25878)
    
    (cherry picked from commit 386d4e0541872984bf2c473f06343a51dc3cf9e1)
---
 .../src/hooks/apiResources/dashboards.ts           |  1 +
 superset/dashboards/schemas.py                     | 20 +++++++++-
 tests/integration_tests/dashboards/api_tests.py    | 43 ++++++++++++++++++++++
 3 files changed, 63 insertions(+), 1 deletion(-)

diff --git a/superset-frontend/src/hooks/apiResources/dashboards.ts b/superset-frontend/src/hooks/apiResources/dashboards.ts
index b21cc668c0..61896ba130 100644
--- a/superset-frontend/src/hooks/apiResources/dashboards.ts
+++ b/superset-frontend/src/hooks/apiResources/dashboards.ts
@@ -31,6 +31,7 @@ export const useDashboard = (idOrSlug: string | number) =>
         (dashboard.json_metadata && JSON.parse(dashboard.json_metadata)) || {},
       position_data:
         dashboard.position_json && JSON.parse(dashboard.position_json),
+      owners: dashboard.owners || [],
     }),
   );
 
diff --git a/superset/dashboards/schemas.py b/superset/dashboards/schemas.py
index 8cbe482edd..615d830d42 100644
--- a/superset/dashboards/schemas.py
+++ b/superset/dashboards/schemas.py
@@ -18,9 +18,10 @@ import json
 import re
 from typing import Any, Union
 
-from marshmallow import fields, post_load, pre_load, Schema
+from marshmallow import fields, post_dump, post_load, pre_load, Schema
 from marshmallow.validate import Length, ValidationError
 
+from superset import security_manager
 from superset.exceptions import SupersetException
 from superset.tags.models import TagType
 from superset.utils import core as utils
@@ -198,6 +199,15 @@ class DashboardGetResponseSchema(Schema):
     changed_on_humanized = fields.String(data_key="changed_on_delta_humanized")
     is_managed_externally = fields.Boolean(allow_none=True, dump_default=False)
 
+    # pylint: disable=unused-argument
+    @post_dump()
+    def post_dump(self, serialized: dict[str, Any], **kwargs: Any) -> dict[str, Any]:
+        if security_manager.is_guest_user():
+            del serialized["owners"]
+            del serialized["changed_by_name"]
+            del serialized["changed_by"]
+        return serialized
+
 
 class DatabaseSchema(Schema):
     id = fields.Int()
@@ -247,6 +257,14 @@ class DashboardDatasetSchema(Schema):
     normalize_columns = fields.Bool()
     always_filter_main_dttm = fields.Bool()
 
+    # pylint: disable=unused-argument
+    @post_dump()
+    def post_dump(self, serialized: dict[str, Any], **kwargs: Any) -> dict[str, Any]:
+        if security_manager.is_guest_user():
+            del serialized["owners"]
+            del serialized["database"]
+        return serialized
+
 
 class BaseDashboardSchema(Schema):
     # pylint: disable=unused-argument
diff --git a/tests/integration_tests/dashboards/api_tests.py b/tests/integration_tests/dashboards/api_tests.py
index cc7bc109b4..a5c44f9f08 100644
--- a/tests/integration_tests/dashboards/api_tests.py
+++ b/tests/integration_tests/dashboards/api_tests.py
@@ -176,6 +176,26 @@ class TestDashboardApi(SupersetTestCase, ApiOwnersTestCaseMixin, InsertChartMixi
         expected_values = [0, 1] if backend() == "presto" else [0, 1, 2]
         self.assertEqual(result[0]["column_types"], expected_values)
 
+    @pytest.mark.usefixtures("load_world_bank_dashboard_with_slices")
+    @patch("superset.dashboards.schemas.security_manager.has_guest_access")
+    @patch("superset.dashboards.schemas.security_manager.is_guest_user")
+    def test_get_dashboard_datasets_as_guest(self, is_guest_user, has_guest_access):
+        self.login(username="admin")
+        uri = "api/v1/dashboard/world_health/datasets"
+        is_guest_user = True
+        has_guest_access = True
+        response = self.get_assert_metric(uri, "get_datasets")
+        self.assertEqual(response.status_code, 200)
+        data = json.loads(response.data.decode("utf-8"))
+        dashboard = Dashboard.get("world_health")
+        expected_dataset_ids = {s.datasource_id for s in dashboard.slices}
+        result = data["result"]
+        actual_dataset_ids = {dataset["id"] for dataset in result}
+        self.assertEqual(actual_dataset_ids, expected_dataset_ids)
+        for dataset in result:
+            for excluded_key in ["database", "owners"]:
+                assert excluded_key not in dataset
+
     @pytest.mark.usefixtures("load_world_bank_dashboard_with_slices")
     def test_get_dashboard_datasets_not_found(self):
         self.login(username="alpha")
@@ -409,6 +429,29 @@ class TestDashboardApi(SupersetTestCase, ApiOwnersTestCaseMixin, InsertChartMixi
         db.session.delete(dashboard)
         db.session.commit()
 
+    @patch("superset.dashboards.schemas.security_manager.has_guest_access")
+    @patch("superset.dashboards.schemas.security_manager.is_guest_user")
+    def test_get_dashboard_as_guest(self, is_guest_user, has_guest_access):
+        """
+        Dashboard API: Test get dashboard as guest
+        """
+        admin = self.get_user("admin")
+        dashboard = self.insert_dashboard(
+            "title", "slug1", [admin.id], created_by=admin
+        )
+        is_guest_user.return_value = True
+        has_guest_access.return_value = True
+        self.login(username="admin")
+        uri = f"api/v1/dashboard/{dashboard.id}"
+        rv = self.get_assert_metric(uri, "get")
+        self.assertEqual(rv.status_code, 200)
+        data = json.loads(rv.data.decode("utf-8"))
+        for excluded_key in ["changed_by", "changed_by_name", "owners"]:
+            assert excluded_key not in data["result"]
+        # rollback changes
+        db.session.delete(dashboard)
+        db.session.commit()
+
     def test_info_dashboard(self):
         """
         Dashboard API: Test info


(superset) 07/11: chore: Adds note about numerical x-axis (#26208)

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 6fa75b70474ff5b6114c019ab22fdc2979d0b322
Author: Michael S. Molina <70...@users.noreply.github.com>
AuthorDate: Thu Dec 7 14:53:33 2023 -0300

    chore: Adds note about numerical x-axis (#26208)
    
    Co-authored-by: John Bodley <45...@users.noreply.github.com>
    (cherry picked from commit b4a35e624b1bb37774aa290018383bbd2bf73ac2)
---
 UPDATING.md | 1 +
 1 file changed, 1 insertion(+)

diff --git a/UPDATING.md b/UPDATING.md
index 38cba8c544..b7e8588eb1 100644
--- a/UPDATING.md
+++ b/UPDATING.md
@@ -24,6 +24,7 @@ assists people when migrating to a new version.
 
 ## 3.1.0
 
+- [26034](https://github.com/apache/superset/issues/26034): Fixes a problem where numeric x-axes were being treated as categorical values. As a consequence of that, the way labels are displayed might change given that ECharts has a different treatment for numerical and categorical values. To revert to the old behavior, users need to manually convert numerical columns to text so that they are treated as categories. Check https://github.com/apache/superset/issues/26159 for more details.
 - [24657](https://github.com/apache/superset/pull/24657): Bumps the cryptography package to augment the OpenSSL security vulnerability.
 
 ### Other