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 2026/08/05 01:32:20 UTC

(superset) branch master updated: fix(sqllab): retry table metadata preview after OAuth2 redirect (#42390)

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

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


The following commit(s) were added to refs/heads/master by this push:
     new 22909bb039e fix(sqllab): retry table metadata preview after OAuth2 redirect (#42390)
22909bb039e is described below

commit 22909bb039e5447d72b13f7660e578bf4754d019
Author: Amin Ghadersohi <am...@gmail.com>
AuthorDate: Tue Aug 4 20:32:13 2026 -0500

    fix(sqllab): retry table metadata preview after OAuth2 redirect (#42390)
    
    Co-authored-by: Claude <no...@anthropic.com>
---
 .../components/TablePreview/TablePreview.test.tsx  | 108 +++++++++++++++++++++
 .../src/SqlLab/components/TablePreview/index.tsx   |  33 +++++--
 .../ErrorMessage/OAuth2RedirectMessage.test.tsx    |   1 +
 .../ErrorMessage/OAuth2RedirectMessage.tsx         |   1 +
 4 files changed, 135 insertions(+), 8 deletions(-)

diff --git a/superset-frontend/src/SqlLab/components/TablePreview/TablePreview.test.tsx b/superset-frontend/src/SqlLab/components/TablePreview/TablePreview.test.tsx
index 07787441f68..32010ea9786 100644
--- a/superset-frontend/src/SqlLab/components/TablePreview/TablePreview.test.tsx
+++ b/superset-frontend/src/SqlLab/components/TablePreview/TablePreview.test.tsx
@@ -18,7 +18,9 @@
  */
 import { type ReactChild } from 'react';
 import fetchMock from 'fetch-mock';
+import { ErrorTypeEnum } from '@superset-ui/core';
 import { table, initialState } from 'src/SqlLab/fixtures';
+import setupErrorMessages from 'src/setup/setupErrorMessages';
 import {
   render,
   waitFor,
@@ -81,6 +83,17 @@ const mockedProps = {
   tableName: table.name,
 };
 
+const OAUTH2_TAB_ID = 'tab-1';
+const oauth2Error = {
+  message: 'The database is currently not authenticated',
+  error_type: ErrorTypeEnum.OAUTH2_REDIRECT,
+  level: 'warning',
+  extra: {
+    url: 'https://example.com/oauth2/authorize',
+    tab_id: OAUTH2_TAB_ID,
+  },
+};
+
 test('renders columns', async () => {
   const { getAllByTestId, queryByText } = render(
     <TablePreview {...mockedProps} />,
@@ -139,6 +152,101 @@ test('renders preview', async () => {
   );
 });
 
+test('renders an OAuth2 authorization prompt when metadata errors with OAUTH2_REDIRECT', async () => {
+  // Register the OAuth2 redirect component so ErrorMessageWithStackTrace can
+  // resolve it for the OAUTH2_REDIRECT error type, mirroring app setup.
+  setupErrorMessages();
+  fetchMock.removeRoutes();
+  fetchMock.get(getTableMetadataEndpoint, {
+    status: 403,
+    body: { errors: [oauth2Error] },
+  });
+  fetchMock.get(getExtraTableMetadataEndpoint, {});
+
+  render(<TablePreview {...mockedProps} />, { useRedux: true, initialState });
+
+  // The structured SupersetError must flow through so the OAuth2 redirect link
+  // renders (and the crud-source retry can re-fetch once the dance completes).
+  const authLink = await screen.findByRole('link', {
+    name: 'provide authorization',
+  });
+  expect(authLink).toHaveAttribute(
+    'href',
+    'https://example.com/oauth2/authorize',
+  );
+});
+
+test('surfaces an OAUTH2_REDIRECT from the extended metadata request when the main request fails generically', async () => {
+  setupErrorMessages();
+  fetchMock.removeRoutes();
+  fetchMock.get(getTableMetadataEndpoint, {
+    status: 500,
+    body: { message: 'Something went wrong' },
+  });
+  fetchMock.get(getExtraTableMetadataEndpoint, {
+    status: 403,
+    body: { errors: [oauth2Error] },
+  });
+
+  render(<TablePreview {...mockedProps} />, { useRedux: true, initialState });
+
+  // The generic failure on the main request must not mask the actionable
+  // OAuth2 payload coming from the extended metadata request.
+  expect(
+    await screen.findByRole('link', { name: 'provide authorization' }),
+  ).toBeInTheDocument();
+});
+
+test('repopulates the preview once the OAuth2 dance completes', async () => {
+  setupErrorMessages();
+  fetchMock.removeRoutes();
+  fetchMock.get(getTableMetadataEndpoint, {
+    status: 403,
+    body: { errors: [oauth2Error] },
+  });
+  fetchMock.get(getExtraTableMetadataEndpoint, {});
+
+  const { getAllByTestId } = render(<TablePreview {...mockedProps} />, {
+    useRedux: true,
+    initialState,
+  });
+  await screen.findByRole('link', { name: 'provide authorization' });
+
+  // The second tab finished authorizing: the database now answers with the
+  // metadata, and the completion broadcast must drive the refetch.
+  fetchMock.removeRoutes();
+  fetchMock.get(getTableMetadataEndpoint, table);
+  fetchMock.get(getExtraTableMetadataEndpoint, {});
+  fireEvent(
+    window,
+    Object.assign(new Event('storage'), {
+      key: 'oauth2_auth_complete',
+      newValue: JSON.stringify({ tabId: OAUTH2_TAB_ID }),
+    }),
+  );
+
+  await waitFor(() =>
+    expect(getAllByTestId('mock-record-row')).toHaveLength(
+      table.columns.length,
+    ),
+  );
+});
+
+test('renders the error message when metadata fails without a structured error', async () => {
+  fetchMock.removeRoutes();
+  fetchMock.get(getTableMetadataEndpoint, {
+    status: 500,
+    body: { message: 'Something went wrong' },
+  });
+  fetchMock.get(getExtraTableMetadataEndpoint, {});
+
+  render(<TablePreview {...mockedProps} />, { useRedux: true, initialState });
+
+  // Without an errors[] array there is no SupersetError to hand to the
+  // registry, so the plain message must still surface to the user.
+  expect(await screen.findByText('Something went wrong')).toBeInTheDocument();
+});
+
 // eslint-disable-next-line no-restricted-globals -- TODO: Migrate from describe blocks
 describe('table actions', () => {
   test('refreshes table metadata when triggered', async () => {
diff --git a/superset-frontend/src/SqlLab/components/TablePreview/index.tsx b/superset-frontend/src/SqlLab/components/TablePreview/index.tsx
index bc53ab7f61e..6aab1e5deb6 100644
--- a/superset-frontend/src/SqlLab/components/TablePreview/index.tsx
+++ b/superset-frontend/src/SqlLab/components/TablePreview/index.tsx
@@ -34,7 +34,11 @@ import {
 import AutoSizer from 'react-virtualized-auto-sizer';
 import { Icons } from '@superset-ui/core/components/Icons';
 import type { SqlLabRootState } from 'src/SqlLab/types';
-import { CopyToClipboard, FilterableTable } from 'src/components';
+import {
+  CopyToClipboard,
+  ErrorMessageWithStackTrace,
+  FilterableTable,
+} from 'src/components';
 import Tabs from '@superset-ui/core/components/Tabs';
 import {
   tableApiUtil,
@@ -257,13 +261,26 @@ const TablePreview: FC<Props> = ({ dbId, catalog, schema, tableName }) => {
   }
 
   if (hasMetadataError || metadataExtrError) {
-    return (
-      <Alert
-        type="warning"
-        message={
-          ((metadataError || metadataExtrError) as ClientErrorObject)?.error
-        }
-      />
+    // Pass the structured SupersetError through to ErrorMessageWithStackTrace so
+    // that OAuth2 redirect errors (error_type=OAUTH2_REDIRECT) render the
+    // "Authorization needed" link and auto-retry once the OAuth2 dance completes.
+    // The source="crud" value drives the tag-invalidation retry in
+    // OAuth2RedirectMessage.tsx (which invalidates the TableMetadatas tag).
+    // Errors without a structured errors[] array (raw 500s, timeouts, network
+    // failures) fall back to the plain message so it isn't swallowed.
+    // Both requests can fail at once, so pick whichever carries a structured
+    // payload rather than always preferring the main request — otherwise a
+    // generic failure there would mask an actionable OAUTH2_REDIRECT from the
+    // extended-metadata request.
+    const clientErrors = [metadataError, metadataExtrError].filter(
+      Boolean,
+    ) as ClientErrorObject[];
+    const errorPayload = clientErrors.find(({ errors }) => errors?.length)
+      ?.errors?.[0];
+    return errorPayload ? (
+      <ErrorMessageWithStackTrace error={errorPayload} source="crud" />
+    ) : (
+      <Alert type="warning" message={clientErrors[0]?.error} />
     );
   }
   if (!data) {
diff --git a/superset-frontend/src/components/ErrorMessage/OAuth2RedirectMessage.test.tsx b/superset-frontend/src/components/ErrorMessage/OAuth2RedirectMessage.test.tsx
index 89726c8af9d..e19492fcc7d 100644
--- a/superset-frontend/src/components/ErrorMessage/OAuth2RedirectMessage.test.tsx
+++ b/superset-frontend/src/components/ErrorMessage/OAuth2RedirectMessage.test.tsx
@@ -271,6 +271,7 @@ describe('OAuth2RedirectMessage Component', () => {
         { type: 'Schemas', id: 'LIST' },
         { type: 'Catalogs', id: 'LIST' },
         'Tables',
+        'TableMetadatas',
       ]);
     });
   });
diff --git a/superset-frontend/src/components/ErrorMessage/OAuth2RedirectMessage.tsx b/superset-frontend/src/components/ErrorMessage/OAuth2RedirectMessage.tsx
index 5466c2adfe7..a03bcf0a2b9 100644
--- a/superset-frontend/src/components/ErrorMessage/OAuth2RedirectMessage.tsx
+++ b/superset-frontend/src/components/ErrorMessage/OAuth2RedirectMessage.tsx
@@ -126,6 +126,7 @@ export function OAuth2RedirectMessage({
             { type: 'Schemas', id: 'LIST' },
             { type: 'Catalogs', id: 'LIST' },
             'Tables',
+            'TableMetadatas',
           ]),
         );
       } else {