mirror of
https://github.com/apache/superset.git
synced 2026-09-01 21:11:28 +00:00
Compare commits
4
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
bdd0ca890b | ||
|
|
3547e7fce5 | ||
|
|
512cb9ccf0 | ||
|
|
5a2060df16 |
@@ -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 () => {
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -231,6 +231,7 @@ describe('OAuth2RedirectMessage Component', () => {
|
||||
{ type: 'Schemas', id: 'LIST' },
|
||||
{ type: 'Catalogs', id: 'LIST' },
|
||||
'Tables',
|
||||
'TableMetadatas',
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -121,6 +121,7 @@ export function OAuth2RedirectMessage({
|
||||
{ type: 'Schemas', id: 'LIST' },
|
||||
{ type: 'Catalogs', id: 'LIST' },
|
||||
'Tables',
|
||||
'TableMetadatas',
|
||||
]),
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user