diff --git a/superset-frontend/packages/superset-ui-core/src/connection/constants.ts b/superset-frontend/packages/superset-ui-core/src/connection/constants.ts index 370c1a50ed..98a8c38574 100644 --- a/superset-frontend/packages/superset-ui-core/src/connection/constants.ts +++ b/superset-frontend/packages/superset-ui-core/src/connection/constants.ts @@ -34,3 +34,8 @@ export const DEFAULT_FETCH_RETRY_OPTIONS: FetchRetryOptions = { retryDelay: 1000, retryOn: [503], }; + +export const COMMON_ERR_MESSAGES = { + SESSION_TIMED_OUT: + 'Your session timed out, please refresh your page and try again.', +}; diff --git a/superset-frontend/packages/superset-ui-core/src/connection/index.ts b/superset-frontend/packages/superset-ui-core/src/connection/index.ts index 594bbcedef..3ac9806a05 100644 --- a/superset-frontend/packages/superset-ui-core/src/connection/index.ts +++ b/superset-frontend/packages/superset-ui-core/src/connection/index.ts @@ -22,4 +22,5 @@ export { default as SupersetClient } from './SupersetClient'; export { default as SupersetClientClass } from './SupersetClientClass'; export * from './types'; +export * from './constants'; export { default as __hack_reexport_connection } from './types'; diff --git a/superset-frontend/src/utils/getClientErrorObject.ts b/superset-frontend/packages/superset-ui-core/src/query/getClientErrorObject.ts similarity index 97% rename from superset-frontend/src/utils/getClientErrorObject.ts rename to superset-frontend/packages/superset-ui-core/src/query/getClientErrorObject.ts index a5f2871872..8b4b62cf76 100644 --- a/superset-frontend/src/utils/getClientErrorObject.ts +++ b/superset-frontend/packages/superset-ui-core/src/query/getClientErrorObject.ts @@ -16,12 +16,14 @@ * specific language governing permissions and limitations * under the License. */ -import { JsonObject, SupersetClientResponse, t } from '@superset-ui/core'; import { + COMMON_ERR_MESSAGES, + JsonObject, + SupersetClientResponse, + t, SupersetError, ErrorTypeEnum, -} from 'src/components/ErrorMessage/types'; -import COMMON_ERR_MESSAGES from './errorMessages'; +} from '@superset-ui/core'; // The response always contains an error attribute, can contain anything from the // SupersetClientResponse object, and can contain a spread JSON blob @@ -86,29 +88,6 @@ export function parseErrorJson(responseObject: JsonObject): ClientErrorObject { return { ...error, error: error.error }; // explicit ClientErrorObject } -/* - * Utility to get standardized error text for generic update failures - */ -export async function getErrorText( - errorObject: ErrorType, - source: ErrorTextSource, -) { - const { error, message } = await getClientErrorObject(errorObject); - let errorText = t('Sorry, an unknown error occurred.'); - - if (error) { - errorText = t( - 'Sorry, there was an error saving this %s: %s', - source, - error, - ); - } - if (typeof message === 'string' && message === 'Forbidden') { - errorText = t('You do not have permission to edit this %s', source); - } - return errorText; -} - export function getClientErrorObject( response: | SupersetClientResponse @@ -203,6 +182,29 @@ export function getClientErrorObject( }); } +/* + * Utility to get standardized error text for generic update failures + */ +export async function getErrorText( + errorObject: ErrorType, + source: ErrorTextSource, +) { + const { error, message } = await getClientErrorObject(errorObject); + let errorText = t('Sorry, an unknown error occurred.'); + + if (error) { + errorText = t( + 'Sorry, there was an error saving this %s: %s', + source, + error, + ); + } + if (typeof message === 'string' && message === 'Forbidden') { + errorText = t('You do not have permission to edit this %s', source); + } + return errorText; +} + export function getClientErrorMessage( message: string, clientError?: ClientErrorObject, diff --git a/superset-frontend/packages/superset-ui-core/src/query/index.ts b/superset-frontend/packages/superset-ui-core/src/query/index.ts index bb83e3d340..434fe95798 100644 --- a/superset-frontend/packages/superset-ui-core/src/query/index.ts +++ b/superset-frontend/packages/superset-ui-core/src/query/index.ts @@ -31,6 +31,7 @@ export { default as normalizeOrderBy } from './normalizeOrderBy'; export { normalizeTimeColumn } from './normalizeTimeColumn'; export { default as extractQueryFields } from './extractQueryFields'; export * from './getXAxis'; +export * from './getClientErrorObject'; export * from './types/AnnotationLayer'; export * from './types/QueryFormData'; diff --git a/superset-frontend/packages/superset-ui-core/src/query/types/Query.ts b/superset-frontend/packages/superset-ui-core/src/query/types/Query.ts index 718f10514c..4d5ccaf6f0 100644 --- a/superset-frontend/packages/superset-ui-core/src/query/types/Query.ts +++ b/superset-frontend/packages/superset-ui-core/src/query/types/Query.ts @@ -166,6 +166,7 @@ export interface QueryContext { form_data?: QueryFormData; } +// Keep in sync with superset/errors.py export const ErrorTypeEnum = { // Frontend errors FRONTEND_CSRF_ERROR: 'FRONTEND_CSRF_ERROR', @@ -187,9 +188,10 @@ export const ErrorTypeEnum = { CONNECTION_UNKNOWN_DATABASE_ERROR: 'CONNECTION_UNKNOWN_DATABASE_ERROR', CONNECTION_DATABASE_PERMISSIONS_ERROR: 'CONNECTION_DATABASE_PERMISSIONS_ERROR', - CONNECTION_MISSING_PARAMETERS_ERRORS: 'CONNECTION_MISSING_PARAMETERS_ERRORS', + CONNECTION_MISSING_PARAMETERS_ERROR: 'CONNECTION_MISSING_PARAMETERS_ERROR', OBJECT_DOES_NOT_EXIST_ERROR: 'OBJECT_DOES_NOT_EXIST_ERROR', SYNTAX_ERROR: 'SYNTAX_ERROR', + CONNECTION_DATABASE_TIMEOUT: 'CONNECTION_DATABASE_TIMEOUT', // Viz errors VIZ_GET_DF_ERROR: 'VIZ_GET_DF_ERROR', @@ -203,12 +205,17 @@ export const ErrorTypeEnum = { DATABASE_SECURITY_ACCESS_ERROR: 'DATABASE_SECURITY_ACCESS_ERROR', QUERY_SECURITY_ACCESS_ERROR: 'QUERY_SECURITY_ACCESS_ERROR', MISSING_OWNERSHIP_ERROR: 'MISSING_OWNERSHIP_ERROR', + USER_ACTIVITY_SECURITY_ACCESS_ERROR: 'USER_ACTIVITY_SECURITY_ACCESS_ERROR', + DASHBOARD_SECURITY_ACCESS_ERROR: 'DASHBOARD_SECURITY_ACCESS_ERROR', + CHART_SECURITY_ACCESS_ERROR: 'CHART_SECURITY_ACCESS_ERROR', + OAUTH2_REDIRECT: 'OAUTH2_REDIRECT', + OAUTH2_REDIRECT_ERROR: 'OAUTH2_REDIRECT_ERROR', // Other errors BACKEND_TIMEOUT_ERROR: 'BACKEND_TIMEOUT_ERROR', DATABASE_NOT_FOUND_ERROR: 'DATABASE_NOT_FOUND_ERROR', - // Sqllab error + // Sql Lab errors MISSING_TEMPLATE_PARAMS_ERROR: 'MISSING_TEMPLATE_PARAMS_ERROR', INVALID_TEMPLATE_PARAMS_ERROR: 'INVALID_TEMPLATE_PARAMS_ERROR', RESULTS_BACKEND_NOT_CONFIGURED_ERROR: 'RESULTS_BACKEND_NOT_CONFIGURED_ERROR', @@ -218,6 +225,8 @@ export const ErrorTypeEnum = { SQLLAB_TIMEOUT_ERROR: 'SQLLAB_TIMEOUT_ERROR', RESULTS_BACKEND_ERROR: 'RESULTS_BACKEND_ERROR', ASYNC_WORKERS_ERROR: 'ASYNC_WORKERS_ERROR', + ADHOC_SUBQUERY_NOT_ALLOWED_ERROR: 'ADHOC_SUBQUERY_NOT_ALLOWED_ERROR', + INVALID_SQL_ERROR: 'INVALID_SQL_ERROR', // Generic errors GENERIC_COMMAND_ERROR: 'GENERIC_COMMAND_ERROR', @@ -226,16 +235,20 @@ export const ErrorTypeEnum = { // API errors INVALID_PAYLOAD_FORMAT_ERROR: 'INVALID_PAYLOAD_FORMAT_ERROR', INVALID_PAYLOAD_SCHEMA_ERROR: 'INVALID_PAYLOAD_SCHEMA_ERROR', + MARSHMALLOW_ERROR: 'MARSHMALLOW_ERROR', + + // Report errors + REPORT_NOTIFICATION_ERROR: 'REPORT_NOTIFICATION_ERROR', } as const; type ValueOf = T[keyof T]; export type ErrorType = ValueOf; -// Keep in sync with superset/views/errors.py +// Keep in sync with superset/errors.py export type ErrorLevel = 'info' | 'warning' | 'error'; -export type ErrorSource = 'dashboard' | 'explore' | 'sqllab'; +export type ErrorSource = 'dashboard' | 'explore' | 'sqllab' | 'crud'; export type SupersetError | null> = { error_type: ErrorType; diff --git a/superset-frontend/packages/superset-ui-core/src/time-comparison/fetchTimeRange.ts b/superset-frontend/packages/superset-ui-core/src/time-comparison/fetchTimeRange.ts new file mode 100644 index 0000000000..50509af52b --- /dev/null +++ b/superset-frontend/packages/superset-ui-core/src/time-comparison/fetchTimeRange.ts @@ -0,0 +1,63 @@ +/** + * 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 rison from 'rison'; +import { SupersetClient, getClientErrorObject } from '@superset-ui/core'; + +export const SEPARATOR = ' : '; + +export const buildTimeRangeString = (since: string, until: string): string => + `${since}${SEPARATOR}${until}`; + +const formatDateEndpoint = (dttm: string, isStart?: boolean): string => + dttm.replace('T00:00:00', '') || (isStart ? '-∞' : '∞'); + +export const formatTimeRange = ( + timeRange: string, + columnPlaceholder = 'col', +) => { + const splitDateRange = timeRange.split(SEPARATOR); + if (splitDateRange.length === 1) return timeRange; + return `${formatDateEndpoint( + splitDateRange[0], + true, + )} ≤ ${columnPlaceholder} < ${formatDateEndpoint(splitDateRange[1])}`; +}; + +export const fetchTimeRange = async ( + timeRange: string, + columnPlaceholder = 'col', +) => { + const query = rison.encode_uri(timeRange); + const endpoint = `/api/v1/time_range/?q=${query}`; + try { + const response = await SupersetClient.get({ endpoint }); + const timeRangeString = buildTimeRangeString( + response?.json?.result[0]?.since || '', + response?.json?.result[0]?.until || '', + ); + return { + value: formatTimeRange(timeRangeString, columnPlaceholder), + }; + } catch (response) { + const clientError = await getClientErrorObject(response); + return { + error: clientError.message || clientError.error || response.statusText, + }; + } +}; diff --git a/superset-frontend/packages/superset-ui-core/src/time-comparison/index.ts b/superset-frontend/packages/superset-ui-core/src/time-comparison/index.ts index 4b9fb361fd..ad5b5f5918 100644 --- a/superset-frontend/packages/superset-ui-core/src/time-comparison/index.ts +++ b/superset-frontend/packages/superset-ui-core/src/time-comparison/index.ts @@ -21,3 +21,4 @@ export * from './types'; export { default as getComparisonInfo } from './getComparisonInfo'; export { default as getComparisonFilters } from './getComparisonFilters'; +export { SEPARATOR, fetchTimeRange } from './fetchTimeRange'; diff --git a/superset-frontend/packages/superset-ui-core/test/chart/components/ChartDataProvider.test.tsx b/superset-frontend/packages/superset-ui-core/test/chart/components/ChartDataProvider.test.tsx index 2947fd9f94..8423ee99ad 100644 --- a/superset-frontend/packages/superset-ui-core/test/chart/components/ChartDataProvider.test.tsx +++ b/superset-frontend/packages/superset-ui-core/test/chart/components/ChartDataProvider.test.tsx @@ -49,14 +49,13 @@ const mockLoadQueryData = jest.fn, unknown[]>( createArrayPromise, ); +const actual = jest.requireActual('../../../src/chart/clients/ChartClient'); // ChartClient is now a mock -jest.mock('../../../src/chart/clients/ChartClient', () => - jest.fn().mockImplementation(() => ({ - loadDatasource: mockLoadDatasource, - loadFormData: mockLoadFormData, - loadQueryData: mockLoadQueryData, - })), -); +jest.spyOn(actual, 'default').mockImplementation(() => ({ + loadDatasource: mockLoadDatasource, + loadFormData: mockLoadFormData, + loadQueryData: mockLoadQueryData, +})); const ChartClientMock = ChartClient as jest.Mock; diff --git a/superset-frontend/packages/superset-ui-core/test/color/SharedLabelColorSingleton.test.ts b/superset-frontend/packages/superset-ui-core/test/color/SharedLabelColorSingleton.test.ts index 8f89ae8a69..ca3f89a523 100644 --- a/superset-frontend/packages/superset-ui-core/test/color/SharedLabelColorSingleton.test.ts +++ b/superset-frontend/packages/superset-ui-core/test/color/SharedLabelColorSingleton.test.ts @@ -25,13 +25,11 @@ import { SharedLabelColor, SharedLabelColorSource, } from '@superset-ui/core'; -import { getAnalogousColors } from '../../src/color/utils'; -jest.mock('../../src/color/utils', () => ({ - getAnalogousColors: jest - .fn() - .mockImplementation(() => ['red', 'green', 'blue']), -})); +const actual = jest.requireActual('../../src/color/utils'); +const getAnalogousColorsSpy = jest + .spyOn(actual, 'getAnalogousColors') + .mockImplementation(() => ['red', 'green', 'blue']); describe('SharedLabelColor', () => { beforeAll(() => { @@ -161,7 +159,7 @@ describe('SharedLabelColor', () => { sharedLabelColor.updateColorMap('', 'testColors'); const colorMap = sharedLabelColor.getColorMap(); expect(Object.fromEntries(colorMap)).not.toEqual({}); - expect(getAnalogousColors).not.toBeCalled(); + expect(getAnalogousColorsSpy).not.toBeCalled(); }); it('should use analagous colors', () => { @@ -176,7 +174,7 @@ describe('SharedLabelColor', () => { sharedLabelColor.updateColorMap('', 'testColors'); const colorMap = sharedLabelColor.getColorMap(); expect(Object.fromEntries(colorMap)).not.toEqual({}); - expect(getAnalogousColors).toBeCalled(); + expect(getAnalogousColorsSpy).toBeCalled(); }); }); diff --git a/superset-frontend/packages/superset-ui-core/test/query/getClientErrorObject.test.ts b/superset-frontend/packages/superset-ui-core/test/query/getClientErrorObject.test.ts new file mode 100644 index 0000000000..c37e6fbe93 --- /dev/null +++ b/superset-frontend/packages/superset-ui-core/test/query/getClientErrorObject.test.ts @@ -0,0 +1,233 @@ +/** + * 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 { + COMMON_ERR_MESSAGES, + getClientErrorMessage, + getClientErrorObject, + getErrorText, + parseErrorJson, + ErrorTypeEnum, +} from '@superset-ui/core'; + +it('Returns a Promise', () => { + const response = getClientErrorObject('error'); + expect(response instanceof Promise).toBe(true); +}); + +it('Returns a Promise that resolves to an object with an error key', async () => { + const error = 'error'; + + const errorObj = await getClientErrorObject(error); + expect(errorObj).toMatchObject({ error }); +}); + +it('Handles Response that can be parsed as json', async () => { + const jsonError = { something: 'something', error: 'Error message' }; + const jsonErrorString = JSON.stringify(jsonError); + + const errorObj = await getClientErrorObject(new Response(jsonErrorString)); + expect(errorObj).toMatchObject(jsonError); +}); + +it('Handles backwards compatibility between old error messages and the new SIP-40 errors format', async () => { + const jsonError = { + errors: [ + { + error_type: ErrorTypeEnum.GENERIC_DB_ENGINE_ERROR, + extra: { engine: 'presto', link: 'https://www.google.com' }, + level: 'error', + message: 'presto error: test error', + }, + ], + }; + const jsonErrorString = JSON.stringify(jsonError); + + const errorObj = await getClientErrorObject(new Response(jsonErrorString)); + expect(errorObj.error).toEqual(jsonError.errors[0].message); + expect(errorObj.link).toEqual(jsonError.errors[0].extra.link); +}); + +it('Handles Response that can be parsed as text', async () => { + const textError = 'Hello I am a text error'; + + const errorObj = await getClientErrorObject(new Response(textError)); + expect(errorObj).toMatchObject({ error: textError }); +}); + +it('Handles TypeError Response', async () => { + const error = new TypeError('Failed to fetch'); + + // @ts-ignore + const errorObj = await getClientErrorObject(error); + expect(errorObj).toMatchObject({ error: 'Network error' }); +}); + +it('Handles timeout error', async () => { + const errorObj = await getClientErrorObject({ + timeout: 1000, + statusText: 'timeout', + }); + expect(errorObj).toMatchObject({ + timeout: 1000, + statusText: 'timeout', + error: 'Request timed out', + errors: [ + { + error_type: ErrorTypeEnum.FRONTEND_TIMEOUT_ERROR, + extra: { + timeout: 1, + issue_codes: [ + { + code: 1000, + message: 'Issue 1000 - The dataset is too large to query.', + }, + { + code: 1001, + message: 'Issue 1001 - The database is under an unusual load.', + }, + ], + }, + level: 'error', + message: 'Request timed out', + }, + ], + }); +}); + +it('Handles plain text as input', async () => { + const error = 'error'; + + const errorObj = await getClientErrorObject(error); + expect(errorObj).toMatchObject({ error }); +}); + +it('Handles error with status text and message', async () => { + const statusText = 'status'; + const message = 'message'; + + // @ts-ignore + expect(await getClientErrorObject({ statusText, message })).toMatchObject({ + error: statusText, + }); + // @ts-ignore + expect(await getClientErrorObject({ message })).toMatchObject({ + error: message, + }); + // @ts-ignore + expect(await getClientErrorObject({})).toMatchObject({ + error: 'An error occurred', + }); +}); + +it('getClientErrorMessage', () => { + expect(getClientErrorMessage('error')).toEqual('error'); + expect( + getClientErrorMessage('error', { + error: 'client error', + message: 'client error message', + }), + ).toEqual('error:\nclient error message'); + expect( + getClientErrorMessage('error', { + error: 'client error', + }), + ).toEqual('error:\nclient error'); +}); + +it('parseErrorJson with message', () => { + expect(parseErrorJson({ message: 'error message' })).toEqual({ + message: 'error message', + error: 'error message', + }); + + expect( + parseErrorJson({ + message: { + key1: ['error message1', 'error message2'], + key2: ['error message3', 'error message4'], + }, + }), + ).toEqual({ + message: { + key1: ['error message1', 'error message2'], + key2: ['error message3', 'error message4'], + }, + error: 'error message1', + }); + + expect( + parseErrorJson({ + message: {}, + }), + ).toEqual({ + message: {}, + error: 'Invalid input', + }); +}); + +it('parseErrorJson with stacktrace', () => { + expect( + parseErrorJson({ error: 'error message', stack: 'stacktrace' }), + ).toEqual({ + error: 'Unexpected error: (no description, click to see stack trace)', + stacktrace: 'stacktrace', + stack: 'stacktrace', + }); + + expect( + parseErrorJson({ + error: 'error message', + description: 'error description', + stack: 'stacktrace', + }), + ).toEqual({ + error: 'Unexpected error: error description', + stacktrace: 'stacktrace', + description: 'error description', + stack: 'stacktrace', + }); +}); + +it('parseErrorJson with CSRF', () => { + expect( + parseErrorJson({ + responseText: 'CSRF', + }), + ).toEqual({ + error: COMMON_ERR_MESSAGES.SESSION_TIMED_OUT, + responseText: 'CSRF', + }); +}); + +it('getErrorText', async () => { + expect(await getErrorText('error', 'dashboard')).toEqual( + 'Sorry, there was an error saving this dashboard: error', + ); + + const error = JSON.stringify({ message: 'Forbidden' }); + expect(await getErrorText(new Response(error), 'dashboard')).toEqual( + 'You do not have permission to edit this dashboard', + ); + expect( + await getErrorText( + new Response(JSON.stringify({ status: 'error' })), + 'dashboard', + ), + ).toEqual('Sorry, an unknown error occurred.'); +}); diff --git a/superset-frontend/packages/superset-ui-core/test/time-comparison/fetchTimeRange.test.ts b/superset-frontend/packages/superset-ui-core/test/time-comparison/fetchTimeRange.test.ts new file mode 100644 index 0000000000..e07fa8617f --- /dev/null +++ b/superset-frontend/packages/superset-ui-core/test/time-comparison/fetchTimeRange.test.ts @@ -0,0 +1,118 @@ +/** + * 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 fetchMock from 'fetch-mock'; +import { fetchTimeRange } from '@superset-ui/core'; +import { + buildTimeRangeString, + formatTimeRange, +} from '../../src/time-comparison/fetchTimeRange'; + +afterEach(fetchMock.restore); + +it('generates proper time range string', () => { + expect( + buildTimeRangeString('2010-07-30T00:00:00', '2020-07-30T00:00:00'), + ).toBe('2010-07-30T00:00:00 : 2020-07-30T00:00:00'); + expect(buildTimeRangeString('', '2020-07-30T00:00:00')).toBe( + ' : 2020-07-30T00:00:00', + ); + expect(buildTimeRangeString('', '')).toBe(' : '); +}); + +it('generates a readable time range', () => { + expect(formatTimeRange('Last 7 days')).toBe('Last 7 days'); + expect(formatTimeRange('No filter')).toBe('No filter'); + expect(formatTimeRange('Yesterday : Tomorrow')).toBe( + 'Yesterday ≤ col < Tomorrow', + ); + expect(formatTimeRange('2010-07-30T00:00:00 : 2020-07-30T00:00:00')).toBe( + '2010-07-30 ≤ col < 2020-07-30', + ); + expect(formatTimeRange('2010-07-30T01:00:00 : ')).toBe( + '2010-07-30T01:00:00 ≤ col < ∞', + ); + expect(formatTimeRange(' : 2020-07-30T00:00:00')).toBe( + '-∞ ≤ col < 2020-07-30', + ); +}); + +it('returns a formatted time range from response', async () => { + fetchMock.get("glob:*/api/v1/time_range/?q='Last+day'", { + result: [ + { + since: '2021-04-13T00:00:00', + until: '2021-04-14T00:00:00', + timeRange: 'Last day', + }, + ], + }); + + const timeRange = await fetchTimeRange('Last day', 'temporal_col'); + expect(timeRange).toEqual({ + value: '2021-04-13 ≤ temporal_col < 2021-04-14', + }); +}); + +it('returns a formatted time range from empty response', async () => { + fetchMock.get("glob:*/api/v1/time_range/?q='Last+day'", { + result: [], + }); + + const timeRange = await fetchTimeRange('Last day'); + expect(timeRange).toEqual({ + value: '-∞ ≤ col < ∞', + }); +}); + +it('returns a formatted error message from response', async () => { + fetchMock.getOnce("glob:*/api/v1/time_range/?q='Last+day'", { + throws: new Response(JSON.stringify({ message: 'Network error' })), + }); + let timeRange = await fetchTimeRange('Last day'); + expect(timeRange).toEqual({ + error: 'Network error', + }); + + fetchMock.getOnce( + "glob:*/api/v1/time_range/?q='Last+day'", + { + throws: new Error('Internal Server Error'), + }, + { overwriteRoutes: true }, + ); + timeRange = await fetchTimeRange('Last day'); + expect(timeRange).toEqual({ + error: 'Internal Server Error', + }); + + fetchMock.getOnce( + "glob:*/api/v1/time_range/?q='Last+day'", + { + throws: new Response(JSON.stringify({ statusText: 'Network error' }), { + statusText: 'Network error', + }), + }, + { overwriteRoutes: true }, + ); + timeRange = await fetchTimeRange('Last day'); + expect(timeRange).toEqual({ + error: 'Network error', + }); +}); diff --git a/superset-frontend/src/SqlLab/actions/sqlLab.js b/superset-frontend/src/SqlLab/actions/sqlLab.js index fa5198e532..6befa17ac1 100644 --- a/superset-frontend/src/SqlLab/actions/sqlLab.js +++ b/superset-frontend/src/SqlLab/actions/sqlLab.js @@ -23,6 +23,8 @@ import { SupersetClient, t, isFeatureEnabled, + COMMON_ERR_MESSAGES, + getClientErrorObject, } from '@superset-ui/core'; import { invert, mapKeys } from 'lodash'; @@ -33,8 +35,6 @@ import { addSuccessToast as addSuccessToastAction, addWarningToast as addWarningToastAction, } from 'src/components/MessageToasts/actions'; -import { getClientErrorObject } from 'src/utils/getClientErrorObject'; -import COMMON_ERR_MESSAGES from 'src/utils/errorMessages'; import { LOG_ACTIONS_SQLLAB_FETCH_FAILED_QUERY } from 'src/logger/LogUtils'; import getBootstrapData from 'src/utils/getBootstrapData'; import { logEvent } from 'src/logger/actions'; diff --git a/superset-frontend/src/SqlLab/components/AceEditorWrapper/useAnnotations.test.ts b/superset-frontend/src/SqlLab/components/AceEditorWrapper/useAnnotations.test.ts index ddabbea55b..0f17fdafcb 100644 --- a/superset-frontend/src/SqlLab/components/AceEditorWrapper/useAnnotations.test.ts +++ b/superset-frontend/src/SqlLab/components/AceEditorWrapper/useAnnotations.test.ts @@ -18,13 +18,13 @@ */ import fetchMock from 'fetch-mock'; import { act, renderHook } from '@testing-library/react-hooks'; +import { COMMON_ERR_MESSAGES } from '@superset-ui/core'; import { createWrapper, defaultStore as store, } from 'spec/helpers/testing-library'; import { api } from 'src/hooks/apiResources/queryApi'; import { initialState } from 'src/SqlLab/fixtures'; -import COMMON_ERR_MESSAGES from 'src/utils/errorMessages'; import { useAnnotations } from './useAnnotations'; const fakeApiResult = { diff --git a/superset-frontend/src/SqlLab/components/AceEditorWrapper/useAnnotations.ts b/superset-frontend/src/SqlLab/components/AceEditorWrapper/useAnnotations.ts index f6924a243f..f640e30779 100644 --- a/superset-frontend/src/SqlLab/components/AceEditorWrapper/useAnnotations.ts +++ b/superset-frontend/src/SqlLab/components/AceEditorWrapper/useAnnotations.ts @@ -18,16 +18,14 @@ */ import { useSelector } from 'react-redux'; +import { COMMON_ERR_MESSAGES, ClientErrorObject, t } from '@superset-ui/core'; import { SqlLabRootState } from 'src/SqlLab/types'; -import COMMON_ERR_MESSAGES from 'src/utils/errorMessages'; import { VALIDATION_DEBOUNCE_MS } from 'src/SqlLab/constants'; import { FetchValidationQueryParams, useQueryValidationsQuery, } from 'src/hooks/apiResources'; import { useDebounceValue } from 'src/hooks/useDebounceValue'; -import { ClientErrorObject } from 'src/utils/getClientErrorObject'; -import { t } from '@superset-ui/core'; export function useAnnotations(params: FetchValidationQueryParams) { const { sql, dbId, schema, templateParams } = params; diff --git a/superset-frontend/src/SqlLab/components/ShareSqlLabQuery/index.tsx b/superset-frontend/src/SqlLab/components/ShareSqlLabQuery/index.tsx index 8f4cde1a66..9309ce7c05 100644 --- a/superset-frontend/src/SqlLab/components/ShareSqlLabQuery/index.tsx +++ b/superset-frontend/src/SqlLab/components/ShareSqlLabQuery/index.tsx @@ -23,13 +23,13 @@ import { t, useTheme, isFeatureEnabled, + getClientErrorObject, } from '@superset-ui/core'; import Button from 'src/components/Button'; import Icons from 'src/components/Icons'; import withToasts from 'src/components/MessageToasts/withToasts'; import CopyToClipboard from 'src/components/CopyToClipboard'; import { storeQuery } from 'src/utils/common'; -import { getClientErrorObject } from 'src/utils/getClientErrorObject'; import useQueryEditor from 'src/SqlLab/hooks/useQueryEditor'; interface ShareSqlLabQueryProps { diff --git a/superset-frontend/src/components/Chart/ChartErrorMessage.tsx b/superset-frontend/src/components/Chart/ChartErrorMessage.tsx index 077ca2282a..b15a20ea67 100644 --- a/superset-frontend/src/components/Chart/ChartErrorMessage.tsx +++ b/superset-frontend/src/components/Chart/ChartErrorMessage.tsx @@ -18,9 +18,9 @@ */ import React from 'react'; +import { SupersetError } from '@superset-ui/core'; import { useChartOwnerNames } from 'src/hooks/apiResources'; import ErrorMessageWithStackTrace from 'src/components/ErrorMessage/ErrorMessageWithStackTrace'; -import { SupersetError } from 'src/components/ErrorMessage/types'; interface Props { chartId: string; diff --git a/superset-frontend/src/components/Chart/chartAction.js b/superset-frontend/src/components/Chart/chartAction.js index 9597a3cbb3..d4657988ca 100644 --- a/superset-frontend/src/components/Chart/chartAction.js +++ b/superset-frontend/src/components/Chart/chartAction.js @@ -25,6 +25,7 @@ import { SupersetClient, t, isFeatureEnabled, + getClientErrorObject, } from '@superset-ui/core'; import { getControlsState } from 'src/explore/store'; import { @@ -38,7 +39,6 @@ import { import { addDangerToast } from 'src/components/MessageToasts/actions'; import { logEvent } from 'src/logger/actions'; import { Logger, LOG_ACTIONS_LOAD_CHART } from 'src/logger/LogUtils'; -import { getClientErrorObject } from 'src/utils/getClientErrorObject'; import { allowCrossDomain as domainShardingEnabled } from 'src/utils/hostNamesConfig'; import { updateDataMask } from 'src/dataMask/actions'; import { waitForAsyncData } from 'src/middleware/asyncEvent'; diff --git a/superset-frontend/src/components/Datasource/ChangeDatasourceModal.tsx b/superset-frontend/src/components/Datasource/ChangeDatasourceModal.tsx index 5e837f0a26..7fde5645a1 100644 --- a/superset-frontend/src/components/Datasource/ChangeDatasourceModal.tsx +++ b/superset-frontend/src/components/Datasource/ChangeDatasourceModal.tsx @@ -24,7 +24,12 @@ import React, { useCallback, } from 'react'; import Alert from 'src/components/Alert'; -import { SupersetClient, t, styled } from '@superset-ui/core'; +import { + SupersetClient, + t, + styled, + getClientErrorObject, +} from '@superset-ui/core'; import TableView, { EmptyWrapperType } from 'src/components/TableView'; import { ServerPagination, SortByType } from 'src/components/TableView/types'; import StyledModal from 'src/components/Modal'; @@ -33,7 +38,6 @@ import { useListViewResource } from 'src/views/CRUD/hooks'; import Dataset from 'src/types/Dataset'; import { useDebouncedEffect } from 'src/explore/exploreUtils'; import { SLOW_DEBOUNCE } from 'src/constants'; -import { getClientErrorObject } from 'src/utils/getClientErrorObject'; import Loading from 'src/components/Loading'; import { AntdInput } from 'src/components'; import { Input } from 'src/components/Input'; diff --git a/superset-frontend/src/components/Datasource/DatasourceEditor.jsx b/superset-frontend/src/components/Datasource/DatasourceEditor.jsx index cc18ec0fe4..b9af27d2af 100644 --- a/superset-frontend/src/components/Datasource/DatasourceEditor.jsx +++ b/superset-frontend/src/components/Datasource/DatasourceEditor.jsx @@ -34,6 +34,7 @@ import { SupersetClient, t, withTheme, + getClientErrorObject, } from '@superset-ui/core'; import { Select, AsyncSelect, Row, Col } from 'src/components'; import { FormLabel } from 'src/components/Form'; @@ -46,7 +47,6 @@ import Label from 'src/components/Label'; import Loading from 'src/components/Loading'; import TableSelector from 'src/components/TableSelector'; import EditableTitle from 'src/components/EditableTitle'; -import { getClientErrorObject } from 'src/utils/getClientErrorObject'; import CheckboxControl from 'src/explore/components/controls/CheckboxControl'; import TextControl from 'src/explore/components/controls/TextControl'; import TextAreaControl from 'src/explore/components/controls/TextAreaControl'; diff --git a/superset-frontend/src/components/Datasource/DatasourceModal.tsx b/superset-frontend/src/components/Datasource/DatasourceModal.tsx index e318821a5b..f4cdecc1c3 100644 --- a/superset-frontend/src/components/Datasource/DatasourceModal.tsx +++ b/superset-frontend/src/components/Datasource/DatasourceModal.tsx @@ -26,16 +26,16 @@ import { Metric, styled, SupersetClient, + getClientErrorObject, t, + SupersetError, } from '@superset-ui/core'; import Modal from 'src/components/Modal'; import AsyncEsmComponent from 'src/components/AsyncEsmComponent'; -import { SupersetError } from 'src/components/ErrorMessage/types'; import ErrorMessageWithStackTrace from 'src/components/ErrorMessage/ErrorMessageWithStackTrace'; import withToasts from 'src/components/MessageToasts/withToasts'; import { useSelector } from 'react-redux'; -import { getClientErrorObject } from 'src/utils/getClientErrorObject'; const DatasourceEditor = AsyncEsmComponent(() => import('./DatasourceEditor')); diff --git a/superset-frontend/src/components/ErrorMessage/BasicErrorAlert.test.tsx b/superset-frontend/src/components/ErrorMessage/BasicErrorAlert.test.tsx index a82f3583cd..e39f5079b6 100644 --- a/superset-frontend/src/components/ErrorMessage/BasicErrorAlert.test.tsx +++ b/superset-frontend/src/components/ErrorMessage/BasicErrorAlert.test.tsx @@ -19,9 +19,8 @@ import React from 'react'; import { render, screen } from 'spec/helpers/testing-library'; -import { supersetTheme } from '@superset-ui/core'; +import { ErrorLevel, supersetTheme } from '@superset-ui/core'; import BasicErrorAlert from './BasicErrorAlert'; -import { ErrorLevel } from './types'; jest.mock( 'src/components/Icons/Icon', diff --git a/superset-frontend/src/components/ErrorMessage/BasicErrorAlert.tsx b/superset-frontend/src/components/ErrorMessage/BasicErrorAlert.tsx index 918aee5f24..db61656b83 100644 --- a/superset-frontend/src/components/ErrorMessage/BasicErrorAlert.tsx +++ b/superset-frontend/src/components/ErrorMessage/BasicErrorAlert.tsx @@ -17,9 +17,8 @@ * under the License. */ import React from 'react'; -import { styled, useTheme } from '@superset-ui/core'; +import { ErrorLevel, styled, useTheme } from '@superset-ui/core'; import Icons from 'src/components/Icons'; -import { ErrorLevel } from './types'; const StyledContainer = styled.div<{ level: ErrorLevel }>` display: flex; diff --git a/superset-frontend/src/components/ErrorMessage/DatabaseErrorMessage.test.tsx b/superset-frontend/src/components/ErrorMessage/DatabaseErrorMessage.test.tsx index 0a1ad27299..1ddcb67aca 100644 --- a/superset-frontend/src/components/ErrorMessage/DatabaseErrorMessage.test.tsx +++ b/superset-frontend/src/components/ErrorMessage/DatabaseErrorMessage.test.tsx @@ -18,10 +18,10 @@ */ import React from 'react'; +import { ErrorLevel, ErrorSource, ErrorTypeEnum } from '@superset-ui/core'; import { render, screen } from 'spec/helpers/testing-library'; import userEvent from '@testing-library/user-event'; import DatabaseErrorMessage from './DatabaseErrorMessage'; -import { ErrorLevel, ErrorSource, ErrorTypeEnum } from './types'; jest.mock( 'src/components/Icons/Icon', diff --git a/superset-frontend/src/components/ErrorMessage/DatasetNotFoundErrorMessage.test.tsx b/superset-frontend/src/components/ErrorMessage/DatasetNotFoundErrorMessage.test.tsx index 60883ae9a0..c258d9128c 100644 --- a/superset-frontend/src/components/ErrorMessage/DatasetNotFoundErrorMessage.test.tsx +++ b/superset-frontend/src/components/ErrorMessage/DatasetNotFoundErrorMessage.test.tsx @@ -18,9 +18,9 @@ */ import React from 'react'; +import { ErrorLevel, ErrorSource, ErrorTypeEnum } from '@superset-ui/core'; import { render, screen } from 'spec/helpers/testing-library'; import DatasetNotFoundErrorMessage from './DatasetNotFoundErrorMessage'; -import { ErrorLevel, ErrorSource, ErrorTypeEnum } from './types'; jest.mock( 'src/components/Icons/Icon', diff --git a/superset-frontend/src/components/ErrorMessage/ErrorAlert.test.tsx b/superset-frontend/src/components/ErrorMessage/ErrorAlert.test.tsx index d6f2ebe25e..65530ff91f 100644 --- a/superset-frontend/src/components/ErrorMessage/ErrorAlert.test.tsx +++ b/superset-frontend/src/components/ErrorMessage/ErrorAlert.test.tsx @@ -20,10 +20,9 @@ import React from 'react'; import userEvent from '@testing-library/user-event'; import { render, screen } from 'spec/helpers/testing-library'; -import { supersetTheme } from '@superset-ui/core'; +import { ErrorLevel, ErrorSource, supersetTheme } from '@superset-ui/core'; import { isCurrentUserBot } from 'src/utils/isBot'; import ErrorAlert from './ErrorAlert'; -import { ErrorLevel, ErrorSource } from './types'; jest.mock( 'src/components/Icons/Icon', diff --git a/superset-frontend/src/components/ErrorMessage/ErrorAlert.tsx b/superset-frontend/src/components/ErrorMessage/ErrorAlert.tsx index da5894e887..c80d78d926 100644 --- a/superset-frontend/src/components/ErrorMessage/ErrorAlert.tsx +++ b/superset-frontend/src/components/ErrorMessage/ErrorAlert.tsx @@ -17,14 +17,19 @@ * under the License. */ import React, { useState, ReactNode } from 'react'; -import { styled, useTheme, t } from '@superset-ui/core'; +import { + ErrorLevel, + ErrorSource, + styled, + useTheme, + t, +} from '@superset-ui/core'; import { noOp } from 'src/utils/common'; import Modal from 'src/components/Modal'; import Button from 'src/components/Button'; import { isCurrentUserBot } from 'src/utils/isBot'; import Icons from 'src/components/Icons'; -import { ErrorLevel, ErrorSource } from './types'; import CopyToClipboard from '../CopyToClipboard'; const ErrorAlertDiv = styled.div<{ level: ErrorLevel }>` diff --git a/superset-frontend/src/components/ErrorMessage/ErrorMessageWithStackTrace.test.tsx b/superset-frontend/src/components/ErrorMessage/ErrorMessageWithStackTrace.test.tsx index 7583983e43..ef505e4ab2 100644 --- a/superset-frontend/src/components/ErrorMessage/ErrorMessageWithStackTrace.test.tsx +++ b/superset-frontend/src/components/ErrorMessage/ErrorMessageWithStackTrace.test.tsx @@ -18,11 +18,11 @@ */ import React from 'react'; +import { ErrorLevel, ErrorSource, ErrorTypeEnum } from '@superset-ui/core'; import { render, screen } from 'spec/helpers/testing-library'; import userEvent from '@testing-library/user-event'; import ErrorMessageWithStackTrace from './ErrorMessageWithStackTrace'; import BasicErrorAlert from './BasicErrorAlert'; -import { ErrorLevel, ErrorSource, ErrorTypeEnum } from './types'; jest.mock( 'src/components/Icons/Icon', diff --git a/superset-frontend/src/components/ErrorMessage/ErrorMessageWithStackTrace.tsx b/superset-frontend/src/components/ErrorMessage/ErrorMessageWithStackTrace.tsx index b9e7e5c053..5197ea72ef 100644 --- a/superset-frontend/src/components/ErrorMessage/ErrorMessageWithStackTrace.tsx +++ b/superset-frontend/src/components/ErrorMessage/ErrorMessageWithStackTrace.tsx @@ -17,9 +17,8 @@ * under the License. */ import React from 'react'; -import { t } from '@superset-ui/core'; +import { ErrorSource, t, SupersetError } from '@superset-ui/core'; import getErrorMessageComponentRegistry from './getErrorMessageComponentRegistry'; -import { SupersetError, ErrorSource } from './types'; import ErrorAlert from './ErrorAlert'; const DEFAULT_TITLE = t('Unexpected error'); diff --git a/superset-frontend/src/components/ErrorMessage/MarshmallowErrorMessage.test.tsx b/superset-frontend/src/components/ErrorMessage/MarshmallowErrorMessage.test.tsx index 5fd6ee7bfb..5a35f93068 100644 --- a/superset-frontend/src/components/ErrorMessage/MarshmallowErrorMessage.test.tsx +++ b/superset-frontend/src/components/ErrorMessage/MarshmallowErrorMessage.test.tsx @@ -20,8 +20,12 @@ import React from 'react'; import { render, screen, fireEvent } from '@testing-library/react'; import '@testing-library/jest-dom/extend-expect'; -import { ThemeProvider, supersetTheme } from '@superset-ui/core'; -import { ErrorLevel, ErrorTypeEnum } from 'src/components/ErrorMessage/types'; +import { + ErrorLevel, + ErrorTypeEnum, + ThemeProvider, + supersetTheme, +} from '@superset-ui/core'; import MarshmallowErrorMessage from './MarshmallowErrorMessage'; describe('MarshmallowErrorMessage', () => { diff --git a/superset-frontend/src/components/ErrorMessage/OAuth2RedirectMessage.test.tsx b/superset-frontend/src/components/ErrorMessage/OAuth2RedirectMessage.test.tsx index c0930ee41b..f9c6872f00 100644 --- a/superset-frontend/src/components/ErrorMessage/OAuth2RedirectMessage.test.tsx +++ b/superset-frontend/src/components/ErrorMessage/OAuth2RedirectMessage.test.tsx @@ -23,13 +23,14 @@ import { Provider } from 'react-redux'; import { createStore } from 'redux'; import { render, fireEvent, waitFor } from '@testing-library/react'; import '@testing-library/jest-dom'; -import { ThemeProvider, supersetTheme } from '@superset-ui/core'; -import OAuth2RedirectMessage from 'src/components/ErrorMessage/OAuth2RedirectMessage'; import { ErrorLevel, ErrorSource, ErrorTypeEnum, -} from 'src/components/ErrorMessage/types'; + ThemeProvider, + supersetTheme, +} from '@superset-ui/core'; +import OAuth2RedirectMessage from 'src/components/ErrorMessage/OAuth2RedirectMessage'; import { reRunQuery } from 'src/SqlLab/actions/sqlLab'; import { triggerQuery } from 'src/components/Chart/chartAction'; import { onRefresh } from 'src/dashboard/actions/dashboardState'; diff --git a/superset-frontend/src/components/ErrorMessage/ParameterErrorMessage.test.tsx b/superset-frontend/src/components/ErrorMessage/ParameterErrorMessage.test.tsx index 0b9518b4fc..5d7b949267 100644 --- a/superset-frontend/src/components/ErrorMessage/ParameterErrorMessage.test.tsx +++ b/superset-frontend/src/components/ErrorMessage/ParameterErrorMessage.test.tsx @@ -18,10 +18,10 @@ */ import userEvent from '@testing-library/user-event'; +import { ErrorLevel, ErrorSource, ErrorTypeEnum } from '@superset-ui/core'; import React from 'react'; import { render, screen } from 'spec/helpers/testing-library'; import ParameterErrorMessage from './ParameterErrorMessage'; -import { ErrorLevel, ErrorSource, ErrorTypeEnum } from './types'; jest.mock( 'src/components/Icons/Icon', diff --git a/superset-frontend/src/components/ErrorMessage/TimeoutErrorMessage.test.tsx b/superset-frontend/src/components/ErrorMessage/TimeoutErrorMessage.test.tsx index a82b4d3f76..a99afed906 100644 --- a/superset-frontend/src/components/ErrorMessage/TimeoutErrorMessage.test.tsx +++ b/superset-frontend/src/components/ErrorMessage/TimeoutErrorMessage.test.tsx @@ -19,9 +19,9 @@ import userEvent from '@testing-library/user-event'; import React from 'react'; +import { ErrorSource, ErrorTypeEnum, ErrorLevel } from '@superset-ui/core'; import { render, screen } from 'spec/helpers/testing-library'; import TimeoutErrorMessage from './TimeoutErrorMessage'; -import { ErrorLevel, ErrorSource, ErrorTypeEnum } from './types'; jest.mock( 'src/components/Icons/Icon', diff --git a/superset-frontend/src/components/ErrorMessage/types.ts b/superset-frontend/src/components/ErrorMessage/types.ts index a27c4aff45..ed05ef4481 100644 --- a/superset-frontend/src/components/ErrorMessage/types.ts +++ b/superset-frontend/src/components/ErrorMessage/types.ts @@ -17,95 +17,14 @@ * under the License. */ -// Keep in sync with superset/views/errors.py -export const ErrorTypeEnum = { - // Frontend errors - FRONTEND_CSRF_ERROR: 'FRONTEND_CSRF_ERROR', - FRONTEND_NETWORK_ERROR: 'FRONTEND_NETWORK_ERROR', - FRONTEND_TIMEOUT_ERROR: 'FRONTEND_TIMEOUT_ERROR', - - // DB Engine errors - GENERIC_DB_ENGINE_ERROR: 'GENERIC_DB_ENGINE_ERROR', - COLUMN_DOES_NOT_EXIST_ERROR: 'COLUMN_DOES_NOT_EXIST_ERROR', - TABLE_DOES_NOT_EXIST_ERROR: 'TABLE_DOES_NOT_EXIST_ERROR', - SCHEMA_DOES_NOT_EXIST_ERROR: 'SCHEMA_DOES_NOT_EXIST_ERROR', - CONNECTION_INVALID_USERNAME_ERROR: 'CONNECTION_INVALID_USERNAME_ERROR', - CONNECTION_INVALID_PASSWORD_ERROR: 'CONNECTION_INVALID_PASSWORD_ERROR', - CONNECTION_INVALID_HOSTNAME_ERROR: 'CONNECTION_INVALID_HOSTNAME_ERROR', - CONNECTION_PORT_CLOSED_ERROR: 'CONNECTION_PORT_CLOSED_ERROR', - CONNECTION_INVALID_PORT_ERROR: 'CONNECTION_INVALID_PORT_ERROR', - CONNECTION_HOST_DOWN_ERROR: 'CONNECTION_HOST_DOWN_ERROR', - CONNECTION_ACCESS_DENIED_ERROR: 'CONNECTION_ACCESS_DENIED_ERROR', - CONNECTION_UNKNOWN_DATABASE_ERROR: 'CONNECTION_UNKNOWN_DATABASE_ERROR', - CONNECTION_DATABASE_PERMISSIONS_ERROR: - 'CONNECTION_DATABASE_PERMISSIONS_ERROR', - CONNECTION_MISSING_PARAMETERS_ERRORS: 'CONNECTION_MISSING_PARAMETERS_ERRORS', - OBJECT_DOES_NOT_EXIST_ERROR: 'OBJECT_DOES_NOT_EXIST_ERROR', - SYNTAX_ERROR: 'SYNTAX_ERROR', - - // Viz errors - VIZ_GET_DF_ERROR: 'VIZ_GET_DF_ERROR', - UNKNOWN_DATASOURCE_TYPE_ERROR: 'UNKNOWN_DATASOURCE_TYPE_ERROR', - FAILED_FETCHING_DATASOURCE_INFO_ERROR: - 'FAILED_FETCHING_DATASOURCE_INFO_ERROR', - - // Security access errors - TABLE_SECURITY_ACCESS_ERROR: 'TABLE_SECURITY_ACCESS_ERROR', - DATASOURCE_SECURITY_ACCESS_ERROR: 'DATASOURCE_SECURITY_ACCESS_ERROR', - DATABASE_SECURITY_ACCESS_ERROR: 'DATABASE_SECURITY_ACCESS_ERROR', - QUERY_SECURITY_ACCESS_ERROR: 'QUERY_SECURITY_ACCESS_ERROR', - MISSING_OWNERSHIP_ERROR: 'MISSING_OWNERSHIP_ERROR', - DASHBOARD_SECURITY_ACCESS_ERROR: 'DASHBOARD_SECURITY_ACCESS_ERROR', - OAUTH2_REDIRECT: 'OAUTH2_REDIRECT', - OAUTH2_REDIRECT_ERROR: 'OAUTH2_REDIRECT_ERROR', - - // Other errors - BACKEND_TIMEOUT_ERROR: 'BACKEND_TIMEOUT_ERROR', - DATABASE_NOT_FOUND_ERROR: 'DATABASE_NOT_FOUND_ERROR', - - // Sqllab error - MISSING_TEMPLATE_PARAMS_ERROR: 'MISSING_TEMPLATE_PARAMS_ERROR', - INVALID_TEMPLATE_PARAMS_ERROR: 'INVALID_TEMPLATE_PARAMS_ERROR', - RESULTS_BACKEND_NOT_CONFIGURED_ERROR: 'RESULTS_BACKEND_NOT_CONFIGURED_ERROR', - DML_NOT_ALLOWED_ERROR: 'DML_NOT_ALLOWED_ERROR', - INVALID_CTAS_QUERY_ERROR: 'INVALID_CTAS_QUERY_ERROR', - INVALID_CVAS_QUERY_ERROR: 'INVALID_CVAS_QUERY_ERROR', - SQLLAB_TIMEOUT_ERROR: 'SQLLAB_TIMEOUT_ERROR', - RESULTS_BACKEND_ERROR: 'RESULTS_BACKEND_ERROR', - ASYNC_WORKERS_ERROR: 'ASYNC_WORKERS_ERROR', - - // Generic errors - GENERIC_COMMAND_ERROR: 'GENERIC_COMMAND_ERROR', - GENERIC_BACKEND_ERROR: 'GENERIC_BACKEND_ERROR', - - // API errors - INVALID_PAYLOAD_FORMAT_ERROR: 'INVALID_PAYLOAD_FORMAT_ERROR', - INVALID_PAYLOAD_SCHEMA_ERROR: 'INVALID_PAYLOAD_SCHEMA_ERROR', - MARSHMALLOW_ERROR: 'MARSHMALLOW_ERROR', -} as const; - -type ValueOf = T[keyof T]; - -export type ErrorType = ValueOf; - -// Keep in sync with superset/views/errors.py -export type ErrorLevel = 'info' | 'warning' | 'error'; - -export type ErrorSource = 'dashboard' | 'explore' | 'sqllab' | 'crud'; - -export type SupersetError | null> = { - error_type: ErrorType; - extra: ExtraType; - level: ErrorLevel; - message: string; -}; +import { ReactNode, ComponentType } from 'react'; +import { ErrorSource, SupersetError } from '@superset-ui/core'; export type ErrorMessageComponentProps | null> = { error: SupersetError; source?: ErrorSource; - subtitle?: React.ReactNode; + subtitle?: ReactNode; }; -export type ErrorMessageComponent = - React.ComponentType; +export type ErrorMessageComponent = ComponentType; diff --git a/superset-frontend/src/components/Select/AsyncSelect.tsx b/superset-frontend/src/components/Select/AsyncSelect.tsx index 3fb8bceaf4..4f7a69784d 100644 --- a/superset-frontend/src/components/Select/AsyncSelect.tsx +++ b/superset-frontend/src/components/Select/AsyncSelect.tsx @@ -29,11 +29,15 @@ import React, { useImperativeHandle, ClipboardEvent, } from 'react'; -import { ensureIsArray, t, usePrevious } from '@superset-ui/core'; +import { + ensureIsArray, + t, + usePrevious, + getClientErrorObject, +} from '@superset-ui/core'; import { LabeledValue as AntdLabeledValue } from 'antd/lib/select'; import { debounce, isEqual, uniq } from 'lodash'; import Icons from 'src/components/Icons'; -import { getClientErrorObject } from 'src/utils/getClientErrorObject'; import { FAST_DEBOUNCE, SLOW_DEBOUNCE } from 'src/constants'; import { getValue, diff --git a/superset-frontend/src/components/TableSelector/index.tsx b/superset-frontend/src/components/TableSelector/index.tsx index 2598d4df23..b7c92a978a 100644 --- a/superset-frontend/src/components/TableSelector/index.tsx +++ b/superset-frontend/src/components/TableSelector/index.tsx @@ -25,7 +25,12 @@ import React, { } from 'react'; import { SelectValue } from 'antd/lib/select'; -import { styled, t } from '@superset-ui/core'; +import { + styled, + t, + getClientErrorMessage, + getClientErrorObject, +} from '@superset-ui/core'; import { Select } from 'src/components'; import { FormLabel } from 'src/components/Form'; import Icons from 'src/components/Icons'; @@ -37,10 +42,6 @@ import CertifiedBadge from 'src/components/CertifiedBadge'; import WarningIconWithTooltip from 'src/components/WarningIconWithTooltip'; import { useToasts } from 'src/components/MessageToasts/withToasts'; import { useTables, Table } from 'src/hooks/apiResources'; -import { - getClientErrorMessage, - getClientErrorObject, -} from 'src/utils/getClientErrorObject'; const REFRESH_WIDTH = 30; diff --git a/superset-frontend/src/components/Tags/utils.tsx b/superset-frontend/src/components/Tags/utils.tsx index ec08a3b126..f0dd4c46f6 100644 --- a/superset-frontend/src/components/Tags/utils.tsx +++ b/superset-frontend/src/components/Tags/utils.tsx @@ -17,15 +17,16 @@ * under the License. */ -import { SupersetClient, t } from '@superset-ui/core'; +import { + ClientErrorObject, + getClientErrorObject, + SupersetClient, + t, +} from '@superset-ui/core'; import Tag from 'src/types/TagType'; import rison from 'rison'; import { cacheWrapper } from 'src/utils/cacheWrapper'; -import { - ClientErrorObject, - getClientErrorObject, -} from 'src/utils/getClientErrorObject'; const localCache = new Map(); diff --git a/superset-frontend/src/dashboard/actions/dashboardInfo.ts b/superset-frontend/src/dashboard/actions/dashboardInfo.ts index 472f945b54..d1029203c7 100644 --- a/superset-frontend/src/dashboard/actions/dashboardInfo.ts +++ b/superset-frontend/src/dashboard/actions/dashboardInfo.ts @@ -17,9 +17,13 @@ * under the License. */ import { Dispatch } from 'redux'; -import { makeApi, CategoricalColorNamespace, t } from '@superset-ui/core'; +import { + makeApi, + CategoricalColorNamespace, + t, + getErrorText, +} from '@superset-ui/core'; import { isString } from 'lodash'; -import { getErrorText } from 'src/utils/getClientErrorObject'; import { addDangerToast } from 'src/components/MessageToasts/actions'; import { ChartConfiguration, diff --git a/superset-frontend/src/dashboard/actions/dashboardState.js b/superset-frontend/src/dashboard/actions/dashboardState.js index bdbf430903..d6c7109a4c 100644 --- a/superset-frontend/src/dashboard/actions/dashboardState.js +++ b/superset-frontend/src/dashboard/actions/dashboardState.js @@ -26,6 +26,7 @@ import { getSharedLabelColor, SupersetClient, t, + getClientErrorObject, } from '@superset-ui/core'; import { addChart, @@ -34,7 +35,6 @@ import { } from 'src/components/Chart/chartAction'; import { chart as initChart } from 'src/components/Chart/chartReducer'; import { applyDefaultFormData } from 'src/explore/store'; -import { getClientErrorObject } from 'src/utils/getClientErrorObject'; import { SAVE_TYPE_OVERWRITE, SAVE_TYPE_OVERWRITE_CONFIRMED, diff --git a/superset-frontend/src/dashboard/actions/sliceEntities.ts b/superset-frontend/src/dashboard/actions/sliceEntities.ts index 9d85e57a6a..91b5faf6f0 100644 --- a/superset-frontend/src/dashboard/actions/sliceEntities.ts +++ b/superset-frontend/src/dashboard/actions/sliceEntities.ts @@ -17,9 +17,13 @@ * under the License. */ import rison from 'rison'; -import { DatasourceType, SupersetClient, t } from '@superset-ui/core'; +import { + DatasourceType, + SupersetClient, + t, + getClientErrorObject, +} from '@superset-ui/core'; import { addDangerToast } from 'src/components/MessageToasts/actions'; -import { getClientErrorObject } from 'src/utils/getClientErrorObject'; import { Dispatch } from 'redux'; import { Slice } from '../types'; diff --git a/superset-frontend/src/dashboard/components/PropertiesModal/index.tsx b/superset-frontend/src/dashboard/components/PropertiesModal/index.tsx index 1b5ef5fceb..8613a8db6f 100644 --- a/superset-frontend/src/dashboard/components/PropertiesModal/index.tsx +++ b/superset-frontend/src/dashboard/components/PropertiesModal/index.tsx @@ -34,6 +34,7 @@ import { styled, SupersetClient, t, + getClientErrorObject, } from '@superset-ui/core'; import Modal from 'src/components/Modal'; @@ -41,7 +42,6 @@ import { JsonEditor } from 'src/components/AsyncAceEditor'; import ColorSchemeControlWrapper from 'src/dashboard/components/ColorSchemeControlWrapper'; import FilterScopeModal from 'src/dashboard/components/filterscope/FilterScopeModal'; -import { getClientErrorObject } from 'src/utils/getClientErrorObject'; import withToasts from 'src/components/MessageToasts/withToasts'; import TagType from 'src/types/TagType'; import { diff --git a/superset-frontend/src/dashboard/components/URLShortLinkButton/index.tsx b/superset-frontend/src/dashboard/components/URLShortLinkButton/index.tsx index 2d13cedcd6..f91790e814 100644 --- a/superset-frontend/src/dashboard/components/URLShortLinkButton/index.tsx +++ b/superset-frontend/src/dashboard/components/URLShortLinkButton/index.tsx @@ -17,14 +17,13 @@ * under the License. */ import React, { useState } from 'react'; -import { t } from '@superset-ui/core'; +import { getClientErrorObject, t } from '@superset-ui/core'; import Popover, { PopoverProps } from 'src/components/Popover'; import CopyToClipboard from 'src/components/CopyToClipboard'; import { getDashboardPermalink } from 'src/utils/urlUtils'; import { useToasts } from 'src/components/MessageToasts/withToasts'; import { useSelector } from 'react-redux'; import { RootState } from 'src/dashboard/types'; -import { getClientErrorObject } from 'src/utils/getClientErrorObject'; export type URLShortLinkButtonProps = { dashboardId: number; diff --git a/superset-frontend/src/dashboard/components/nativeFilters/FilterBar/FilterControls/FilterValue.tsx b/superset-frontend/src/dashboard/components/nativeFilters/FilterBar/FilterControls/FilterValue.tsx index 5041fa3c97..f8f4f6174e 100644 --- a/superset-frontend/src/dashboard/components/nativeFilters/FilterBar/FilterControls/FilterValue.tsx +++ b/superset-frontend/src/dashboard/components/nativeFilters/FilterBar/FilterControls/FilterValue.tsx @@ -35,6 +35,8 @@ import { styled, SuperChart, t, + ClientErrorObject, + getClientErrorObject, } from '@superset-ui/core'; import { useDispatch, useSelector } from 'react-redux'; import { isEqual, isEqualWith } from 'lodash'; @@ -43,10 +45,6 @@ import Loading from 'src/components/Loading'; import BasicErrorAlert from 'src/components/ErrorMessage/BasicErrorAlert'; import ErrorMessageWithStackTrace from 'src/components/ErrorMessage/ErrorMessageWithStackTrace'; import { waitForAsyncData } from 'src/middleware/asyncEvent'; -import { - ClientErrorObject, - getClientErrorObject, -} from 'src/utils/getClientErrorObject'; import { FilterBarOrientation, RootState } from 'src/dashboard/types'; import { onFiltersRefreshSuccess, diff --git a/superset-frontend/src/dashboard/components/nativeFilters/FiltersConfigModal/FiltersConfigForm/ColumnSelect.test.tsx b/superset-frontend/src/dashboard/components/nativeFilters/FiltersConfigModal/FiltersConfigForm/ColumnSelect.test.tsx index 096771e44d..07e1432733 100644 --- a/superset-frontend/src/dashboard/components/nativeFilters/FiltersConfigModal/FiltersConfigForm/ColumnSelect.test.tsx +++ b/superset-frontend/src/dashboard/components/nativeFilters/FiltersConfigModal/FiltersConfigForm/ColumnSelect.test.tsx @@ -19,7 +19,7 @@ import React from 'react'; import { render, screen, waitFor } from 'spec/helpers/testing-library'; import fetchMock from 'fetch-mock'; -import * as utils from 'src/utils/getClientErrorObject'; +import * as uiCore from '@superset-ui/core'; import { Column, JsonObject } from '@superset-ui/core'; import userEvent from '@testing-library/user-event'; import { ColumnSelect } from './ColumnSelect'; @@ -97,7 +97,7 @@ test('Should call "getClientErrorObject" when api returns an error', async () => const props = createProps(); props.datasetId = 789; - const spy = jest.spyOn(utils, 'getClientErrorObject'); + const spy = jest.spyOn(uiCore, 'getClientErrorObject'); expect(spy).not.toBeCalled(); render(, { diff --git a/superset-frontend/src/dashboard/components/nativeFilters/FiltersConfigModal/FiltersConfigForm/ColumnSelect.tsx b/superset-frontend/src/dashboard/components/nativeFilters/FiltersConfigModal/FiltersConfigForm/ColumnSelect.tsx index d167a9be8c..243f3d2135 100644 --- a/superset-frontend/src/dashboard/components/nativeFilters/FiltersConfigModal/FiltersConfigForm/ColumnSelect.tsx +++ b/superset-frontend/src/dashboard/components/nativeFilters/FiltersConfigModal/FiltersConfigForm/ColumnSelect.tsx @@ -18,10 +18,15 @@ */ import React, { useCallback, useState, useMemo, useEffect } from 'react'; import rison from 'rison'; -import { Column, ensureIsArray, t, useChangeEffect } from '@superset-ui/core'; +import { + Column, + ensureIsArray, + t, + useChangeEffect, + getClientErrorObject, +} from '@superset-ui/core'; import { Select, FormInstance } from 'src/components'; import { useToasts } from 'src/components/MessageToasts/withToasts'; -import { getClientErrorObject } from 'src/utils/getClientErrorObject'; import { cachedSupersetGet } from 'src/utils/cachedSupersetGet'; import { NativeFiltersForm } from '../types'; diff --git a/superset-frontend/src/dashboard/components/nativeFilters/FiltersConfigModal/FiltersConfigForm/DatasetSelect.tsx b/superset-frontend/src/dashboard/components/nativeFilters/FiltersConfigModal/FiltersConfigForm/DatasetSelect.tsx index 532547e58a..a27d6d089a 100644 --- a/superset-frontend/src/dashboard/components/nativeFilters/FiltersConfigModal/FiltersConfigForm/DatasetSelect.tsx +++ b/superset-frontend/src/dashboard/components/nativeFilters/FiltersConfigModal/FiltersConfigForm/DatasetSelect.tsx @@ -18,12 +18,13 @@ */ import React, { useCallback, useMemo, ReactNode } from 'react'; import rison from 'rison'; -import { t, JsonResponse } from '@superset-ui/core'; -import { AsyncSelect } from 'src/components'; import { + t, + JsonResponse, ClientErrorObject, getClientErrorObject, -} from 'src/utils/getClientErrorObject'; +} from '@superset-ui/core'; +import { AsyncSelect } from 'src/components'; import { cachedSupersetGet } from 'src/utils/cachedSupersetGet'; import { Dataset, diff --git a/superset-frontend/src/dashboard/components/nativeFilters/FiltersConfigModal/FiltersConfigForm/FiltersConfigForm.tsx b/superset-frontend/src/dashboard/components/nativeFilters/FiltersConfigModal/FiltersConfigForm/FiltersConfigForm.tsx index 029a95f3ab..be8311b913 100644 --- a/superset-frontend/src/dashboard/components/nativeFilters/FiltersConfigModal/FiltersConfigForm/FiltersConfigForm.tsx +++ b/superset-frontend/src/dashboard/components/nativeFilters/FiltersConfigModal/FiltersConfigForm/FiltersConfigForm.tsx @@ -37,6 +37,8 @@ import { styled, SupersetApiError, t, + ClientErrorObject, + getClientErrorObject, } from '@superset-ui/core'; import { isEqual } from 'lodash'; import React, { @@ -73,10 +75,6 @@ import { import DateFilterControl from 'src/explore/components/controls/DateFilterControl'; import AdhocFilterControl from 'src/explore/components/controls/FilterControl/AdhocFilterControl'; import { waitForAsyncData } from 'src/middleware/asyncEvent'; -import { - ClientErrorObject, - getClientErrorObject, -} from 'src/utils/getClientErrorObject'; import { SingleValueType } from 'src/filters/components/Range/SingleValueType'; import { getFormData, diff --git a/superset-frontend/src/explore/actions/datasourcesActions.test.ts b/superset-frontend/src/explore/actions/datasourcesActions.test.ts index a844ff4789..71a92cfd21 100644 --- a/superset-frontend/src/explore/actions/datasourcesActions.test.ts +++ b/superset-frontend/src/explore/actions/datasourcesActions.test.ts @@ -18,13 +18,13 @@ */ import { DatasourceType } from '@superset-ui/core'; import fetchMock from 'fetch-mock'; +import * as uiCore from '@superset-ui/core'; import { setDatasource, changeDatasource, saveDataset, } from 'src/explore/actions/datasourcesActions'; import sinon from 'sinon'; -import * as ClientError from 'src/utils/getClientErrorObject'; import datasourcesReducer from '../reducers/datasourcesReducer'; import { updateFormDataByDatasource } from './exploreActions'; @@ -126,7 +126,7 @@ test('updateSlice with add to existing dashboard handles failure', async () => { const sampleError = new Error('sampleError'); fetchMock.post(saveDatasetEndpoint, { throws: sampleError }); const dispatch = sinon.spy(); - const errorSpy = jest.spyOn(ClientError, 'getClientErrorObject'); + const errorSpy = jest.spyOn(uiCore, 'getClientErrorObject'); let caughtError; try { diff --git a/superset-frontend/src/explore/actions/datasourcesActions.ts b/superset-frontend/src/explore/actions/datasourcesActions.ts index c11be07be3..54990c6f38 100644 --- a/superset-frontend/src/explore/actions/datasourcesActions.ts +++ b/superset-frontend/src/explore/actions/datasourcesActions.ts @@ -20,9 +20,8 @@ import { Dispatch, AnyAction } from 'redux'; import { ThunkDispatch } from 'redux-thunk'; import { Dataset } from '@superset-ui/chart-controls'; -import { SupersetClient } from '@superset-ui/core'; +import { SupersetClient, getClientErrorObject } from '@superset-ui/core'; import { addDangerToast } from 'src/components/MessageToasts/actions'; -import { getClientErrorObject } from 'src/utils/getClientErrorObject'; import { updateFormDataByDatasource } from './exploreActions'; import { ExplorePageState } from '../types'; diff --git a/superset-frontend/src/explore/components/DataTablesPane/components/useResultsPane.tsx b/superset-frontend/src/explore/components/DataTablesPane/components/useResultsPane.tsx index bae6b6fd7b..bab7b78957 100644 --- a/superset-frontend/src/explore/components/DataTablesPane/components/useResultsPane.tsx +++ b/superset-frontend/src/explore/components/DataTablesPane/components/useResultsPane.tsx @@ -22,11 +22,11 @@ import { styled, t, getChartMetadataRegistry, + getClientErrorObject, } from '@superset-ui/core'; import Loading from 'src/components/Loading'; import { EmptyStateMedium } from 'src/components/EmptyState'; import { getChartDataRequest } from 'src/components/Chart/chartAction'; -import { getClientErrorObject } from 'src/utils/getClientErrorObject'; import { ResultsPaneProps, QueryResultInterface } from '../types'; import { SingleQueryResultPane } from './SingleQueryResultPane'; import { TableControls } from './DataTableControls'; diff --git a/superset-frontend/src/explore/components/PropertiesModal/index.tsx b/superset-frontend/src/explore/components/PropertiesModal/index.tsx index ba78dfa037..45af79a54b 100644 --- a/superset-frontend/src/explore/components/PropertiesModal/index.tsx +++ b/superset-frontend/src/explore/components/PropertiesModal/index.tsx @@ -29,9 +29,9 @@ import { styled, isFeatureEnabled, FeatureFlag, + getClientErrorObject, } from '@superset-ui/core'; import Chart, { Slice } from 'src/types/Chart'; -import { getClientErrorObject } from 'src/utils/getClientErrorObject'; import withToasts from 'src/components/MessageToasts/withToasts'; import { loadTags } from 'src/components/Tags/utils'; import { diff --git a/superset-frontend/src/explore/components/controls/DateFilterControl/DateFilterLabel.tsx b/superset-frontend/src/explore/components/controls/DateFilterControl/DateFilterLabel.tsx index a1a0a670f6..059bb35828 100644 --- a/superset-frontend/src/explore/components/controls/DateFilterControl/DateFilterLabel.tsx +++ b/superset-frontend/src/explore/components/controls/DateFilterControl/DateFilterLabel.tsx @@ -25,6 +25,7 @@ import { NO_TIME_RANGE, SupersetTheme, useCSSTextTruncation, + fetchTimeRange, } from '@superset-ui/core'; import Button from 'src/components/Button'; import ControlHeader from 'src/explore/components/ControlHeader'; @@ -41,7 +42,6 @@ import ControlPopover from '../ControlPopover/ControlPopover'; import { DateFilterControlProps, FrameType } from './types'; import { DateFilterTestKey, - fetchTimeRange, FRAME_OPTIONS, guessFrame, useDefaultTimeFilter, diff --git a/superset-frontend/src/explore/components/controls/DateFilterControl/components/AdvancedFrame.tsx b/superset-frontend/src/explore/components/controls/DateFilterControl/components/AdvancedFrame.tsx index 77f42598ac..88d36f23ce 100644 --- a/superset-frontend/src/explore/components/controls/DateFilterControl/components/AdvancedFrame.tsx +++ b/superset-frontend/src/explore/components/controls/DateFilterControl/components/AdvancedFrame.tsx @@ -17,8 +17,7 @@ * under the License. */ import React from 'react'; -import { t } from '@superset-ui/core'; -import { SEPARATOR } from 'src/explore/components/controls/DateFilterControl/utils'; +import { SEPARATOR, t } from '@superset-ui/core'; import { Input } from 'src/components/Input'; import { InfoTooltipWithTrigger } from '@superset-ui/chart-controls'; import { FrameComponentProps } from 'src/explore/components/controls/DateFilterControl/types'; diff --git a/superset-frontend/src/explore/components/controls/DateFilterControl/tests/utils.test.ts b/superset-frontend/src/explore/components/controls/DateFilterControl/tests/utils.test.ts index 3a0debfec8..d44b08dddd 100644 --- a/superset-frontend/src/explore/components/controls/DateFilterControl/tests/utils.test.ts +++ b/superset-frontend/src/explore/components/controls/DateFilterControl/tests/utils.test.ts @@ -20,8 +20,6 @@ import { customTimeRangeEncode, customTimeRangeDecode, - buildTimeRangeString, - formatTimeRange, } from 'src/explore/components/controls/DateFilterControl/utils'; describe('Custom TimeRange', () => { @@ -298,34 +296,3 @@ describe('Custom TimeRange', () => { }); }); }); - -describe('buildTimeRangeString', () => { - it('generates proper time range string', () => { - expect( - buildTimeRangeString('2010-07-30T00:00:00', '2020-07-30T00:00:00'), - ).toBe('2010-07-30T00:00:00 : 2020-07-30T00:00:00'); - expect(buildTimeRangeString('', '2020-07-30T00:00:00')).toBe( - ' : 2020-07-30T00:00:00', - ); - expect(buildTimeRangeString('', '')).toBe(' : '); - }); -}); - -describe('formatTimeRange', () => { - it('generates a readable time range', () => { - expect(formatTimeRange('Last 7 days')).toBe('Last 7 days'); - expect(formatTimeRange('No filter')).toBe('No filter'); - expect(formatTimeRange('Yesterday : Tomorrow')).toBe( - 'Yesterday ≤ col < Tomorrow', - ); - expect(formatTimeRange('2010-07-30T00:00:00 : 2020-07-30T00:00:00')).toBe( - '2010-07-30 ≤ col < 2020-07-30', - ); - expect(formatTimeRange('2010-07-30T01:00:00 : ')).toBe( - '2010-07-30T01:00:00 ≤ col < ∞', - ); - expect(formatTimeRange(' : 2020-07-30T00:00:00')).toBe( - '-∞ ≤ col < 2020-07-30', - ); - }); -}); diff --git a/superset-frontend/src/explore/components/controls/DateFilterControl/utils/dateFilterUtils.ts b/superset-frontend/src/explore/components/controls/DateFilterControl/utils/dateFilterUtils.ts index aad2f9984d..4be932e34a 100644 --- a/superset-frontend/src/explore/components/controls/DateFilterControl/utils/dateFilterUtils.ts +++ b/superset-frontend/src/explore/components/controls/DateFilterControl/utils/dateFilterUtils.ts @@ -16,9 +16,7 @@ * specific language governing permissions and limitations * under the License. */ -import rison from 'rison'; -import { SupersetClient, NO_TIME_RANGE, JsonObject } from '@superset-ui/core'; -import { getClientErrorObject } from 'src/utils/getClientErrorObject'; +import { NO_TIME_RANGE, JsonObject } from '@superset-ui/core'; import { useSelector } from 'react-redux'; import { COMMON_RANGE_VALUES_SET, @@ -27,26 +25,6 @@ import { } from '.'; import { FrameType } from '../types'; -export const SEPARATOR = ' : '; - -export const buildTimeRangeString = (since: string, until: string): string => - `${since}${SEPARATOR}${until}`; - -const formatDateEndpoint = (dttm: string, isStart?: boolean): string => - dttm.replace('T00:00:00', '') || (isStart ? '-∞' : '∞'); - -export const formatTimeRange = ( - timeRange: string, - columnPlaceholder = 'col', -) => { - const splitDateRange = timeRange.split(SEPARATOR); - if (splitDateRange.length === 1) return timeRange; - return `${formatDateEndpoint( - splitDateRange[0], - true, - )} ≤ ${columnPlaceholder} < ${formatDateEndpoint(splitDateRange[1])}`; -}; - export const guessFrame = (timeRange: string): FrameType => { if (COMMON_RANGE_VALUES_SET.has(timeRange)) { return 'Common'; @@ -63,29 +41,6 @@ export const guessFrame = (timeRange: string): FrameType => { return 'Advanced'; }; -export const fetchTimeRange = async ( - timeRange: string, - columnPlaceholder = 'col', -) => { - const query = rison.encode_uri(timeRange); - const endpoint = `/api/v1/time_range/?q=${query}`; - try { - const response = await SupersetClient.get({ endpoint }); - const timeRangeString = buildTimeRangeString( - response?.json?.result[0]?.since || '', - response?.json?.result[0]?.until || '', - ); - return { - value: formatTimeRange(timeRangeString, columnPlaceholder), - }; - } catch (response) { - const clientError = await getClientErrorObject(response); - return { - error: clientError.message || clientError.error || response.statusText, - }; - } -}; - export function useDefaultTimeFilter() { return ( useSelector( diff --git a/superset-frontend/src/explore/components/controls/DateFilterControl/utils/dateParser.ts b/superset-frontend/src/explore/components/controls/DateFilterControl/utils/dateParser.ts index e4863665bf..b55cceccea 100644 --- a/superset-frontend/src/explore/components/controls/DateFilterControl/utils/dateParser.ts +++ b/superset-frontend/src/explore/components/controls/DateFilterControl/utils/dateParser.ts @@ -17,13 +17,13 @@ * under the License. */ import moment, { Moment } from 'moment'; +import { SEPARATOR } from '@superset-ui/core'; import { CustomRangeDecodeType, CustomRangeType, DateTimeGrainType, DateTimeModeType, } from 'src/explore/components/controls/DateFilterControl/types'; -import { SEPARATOR } from './dateFilterUtils'; import { SEVEN_DAYS_AGO, MIDNIGHT, MOMENT_FORMAT } from './constants'; /** diff --git a/superset-frontend/src/explore/components/controls/FilterControl/utils/useGetTimeRangeLabel.test.ts b/superset-frontend/src/explore/components/controls/FilterControl/utils/useGetTimeRangeLabel.test.ts index 1c3dc321a8..7c0c916109 100644 --- a/superset-frontend/src/explore/components/controls/FilterControl/utils/useGetTimeRangeLabel.test.ts +++ b/superset-frontend/src/explore/components/controls/FilterControl/utils/useGetTimeRangeLabel.test.ts @@ -18,8 +18,8 @@ */ import { renderHook } from '@testing-library/react-hooks'; import { NO_TIME_RANGE } from '@superset-ui/core'; +import * as uiCore from '@superset-ui/core'; import { Operators } from 'src/explore/constants'; -import * as FetchTimeRangeModule from 'src/explore/components/controls/DateFilterControl'; import { useGetTimeRangeLabel } from './useGetTimeRangeLabel'; import AdhocFilter from '../AdhocFilter'; import { Clauses, ExpressionTypes } from '../types'; @@ -65,7 +65,7 @@ test('should get "No filter" label', () => { test('should get actualTimeRange and title', async () => { jest - .spyOn(FetchTimeRangeModule, 'fetchTimeRange') + .spyOn(uiCore, 'fetchTimeRange') .mockResolvedValue({ value: 'MOCK TIME' }); const adhocFilter = new AdhocFilter({ @@ -85,7 +85,7 @@ test('should get actualTimeRange and title', async () => { test('should get actualTimeRange and title when gets an error', async () => { jest - .spyOn(FetchTimeRangeModule, 'fetchTimeRange') + .spyOn(uiCore, 'fetchTimeRange') .mockResolvedValue({ error: 'MOCK ERROR' }); const adhocFilter = new AdhocFilter({ diff --git a/superset-frontend/src/explore/components/controls/FilterControl/utils/useGetTimeRangeLabel.tsx b/superset-frontend/src/explore/components/controls/FilterControl/utils/useGetTimeRangeLabel.tsx index 3a0b7a45ca..d68239c709 100644 --- a/superset-frontend/src/explore/components/controls/FilterControl/utils/useGetTimeRangeLabel.tsx +++ b/superset-frontend/src/explore/components/controls/FilterControl/utils/useGetTimeRangeLabel.tsx @@ -17,8 +17,7 @@ * under the License. */ import { useEffect, useState } from 'react'; -import { NO_TIME_RANGE } from '@superset-ui/core'; -import { fetchTimeRange } from 'src/explore/components/controls/DateFilterControl'; +import { NO_TIME_RANGE, fetchTimeRange } from '@superset-ui/core'; import { Operators } from 'src/explore/constants'; import AdhocFilter from '../AdhocFilter'; import { ExpressionTypes } from '../types'; diff --git a/superset-frontend/src/explore/components/controls/SelectAsyncControl/index.tsx b/superset-frontend/src/explore/components/controls/SelectAsyncControl/index.tsx index ddc242a764..702abf5479 100644 --- a/superset-frontend/src/explore/components/controls/SelectAsyncControl/index.tsx +++ b/superset-frontend/src/explore/components/controls/SelectAsyncControl/index.tsx @@ -17,13 +17,12 @@ * under the License. */ import React, { useEffect, useState } from 'react'; -import { t, SupersetClient } from '@superset-ui/core'; +import { t, SupersetClient, getClientErrorObject } from '@superset-ui/core'; import ControlHeader from 'src/explore/components/ControlHeader'; import { Select } from 'src/components'; import { SelectOptionsType, SelectProps } from 'src/components/Select/types'; import { SelectValue, LabeledValue } from 'antd/lib/select'; import withToasts from 'src/components/MessageToasts/withToasts'; -import { getClientErrorObject } from 'src/utils/getClientErrorObject'; type SelectAsyncProps = Omit; diff --git a/superset-frontend/src/explore/components/controls/ViewQueryModal.tsx b/superset-frontend/src/explore/components/controls/ViewQueryModal.tsx index 023c1e3551..bd07ff5f01 100644 --- a/superset-frontend/src/explore/components/controls/ViewQueryModal.tsx +++ b/superset-frontend/src/explore/components/controls/ViewQueryModal.tsx @@ -17,9 +17,13 @@ * under the License. */ import React, { useEffect, useState } from 'react'; -import { styled, ensureIsArray, t } from '@superset-ui/core'; +import { + styled, + ensureIsArray, + t, + getClientErrorObject, +} from '@superset-ui/core'; import Loading from 'src/components/Loading'; -import { getClientErrorObject } from 'src/utils/getClientErrorObject'; import { getChartDataRequest } from 'src/components/Chart/chartAction'; import ViewQuery from 'src/explore/components/controls/ViewQuery'; diff --git a/superset-frontend/src/features/reports/ReportModal/index.tsx b/superset-frontend/src/features/reports/ReportModal/index.tsx index c9993c8ffa..2f79e31f59 100644 --- a/superset-frontend/src/features/reports/ReportModal/index.tsx +++ b/superset-frontend/src/features/reports/ReportModal/index.tsx @@ -23,9 +23,8 @@ import React, { useCallback, useMemo, } from 'react'; -import { t, SupersetTheme } from '@superset-ui/core'; +import { t, SupersetTheme, getClientErrorObject } from '@superset-ui/core'; import { useDispatch, useSelector } from 'react-redux'; -import { getClientErrorObject } from 'src/utils/getClientErrorObject'; import { addReport, editReport, diff --git a/superset-frontend/src/hooks/apiResources/queryApi.ts b/superset-frontend/src/hooks/apiResources/queryApi.ts index ae25f3cc56..439418529f 100644 --- a/superset-frontend/src/hooks/apiResources/queryApi.ts +++ b/superset-frontend/src/hooks/apiResources/queryApi.ts @@ -17,12 +17,10 @@ * under the License. */ import rison from 'rison'; +import { createApi, BaseQueryFn } from '@reduxjs/toolkit/query/react'; import { ClientErrorObject, getClientErrorObject, -} from 'src/utils/getClientErrorObject'; -import { createApi, BaseQueryFn } from '@reduxjs/toolkit/query/react'; -import { SupersetClient, ParseMethod, SupersetClientResponse, diff --git a/superset-frontend/src/middleware/asyncEvent.test.ts b/superset-frontend/src/middleware/asyncEvent.test.ts index c819893580..1debb22ac0 100644 --- a/superset-frontend/src/middleware/asyncEvent.test.ts +++ b/superset-frontend/src/middleware/asyncEvent.test.ts @@ -20,7 +20,6 @@ import fetchMock from 'fetch-mock'; import WS from 'jest-websocket-mock'; import sinon from 'sinon'; import * as uiCore from '@superset-ui/core'; -import { parseErrorJson } from 'src/utils/getClientErrorObject'; import * as asyncEvent from 'src/middleware/asyncEvent'; describe('asyncEvent middleware', () => { @@ -129,7 +128,7 @@ describe('asyncEvent middleware', () => { status: 200, body: { result: [asyncErrorEvent] }, }); - const errorResponse = await parseErrorJson(asyncErrorEvent); + const errorResponse = await uiCore.parseErrorJson(asyncErrorEvent); await expect( asyncEvent.waitForAsyncData(asyncPendingEvent), ).rejects.toEqual(errorResponse); @@ -204,7 +203,7 @@ describe('asyncEvent middleware', () => { wsServer.send(JSON.stringify(asyncErrorEvent)); - const errorResponse = await parseErrorJson(asyncErrorEvent); + const errorResponse = await uiCore.parseErrorJson(asyncErrorEvent); await expect(promise).rejects.toEqual(errorResponse); diff --git a/superset-frontend/src/middleware/asyncEvent.ts b/superset-frontend/src/middleware/asyncEvent.ts index faa14229ca..0512e6817b 100644 --- a/superset-frontend/src/middleware/asyncEvent.ts +++ b/superset-frontend/src/middleware/asyncEvent.ts @@ -23,13 +23,11 @@ import { makeApi, SupersetClient, logging, -} from '@superset-ui/core'; -import { SupersetError } from 'src/components/ErrorMessage/types'; -import getBootstrapData from 'src/utils/getBootstrapData'; -import { getClientErrorObject, parseErrorJson, -} from '../utils/getClientErrorObject'; + SupersetError, +} from '@superset-ui/core'; +import getBootstrapData from 'src/utils/getBootstrapData'; type AsyncEvent = { id?: string | null; diff --git a/superset-frontend/src/pages/AnnotationList/index.tsx b/superset-frontend/src/pages/AnnotationList/index.tsx index e04b48080f..344b7104ed 100644 --- a/superset-frontend/src/pages/AnnotationList/index.tsx +++ b/superset-frontend/src/pages/AnnotationList/index.tsx @@ -19,7 +19,13 @@ import React, { useMemo, useState, useEffect, useCallback } from 'react'; import { useParams, Link, useHistory } from 'react-router-dom'; -import { css, t, styled, SupersetClient } from '@superset-ui/core'; +import { + css, + t, + styled, + SupersetClient, + getClientErrorObject, +} from '@superset-ui/core'; import moment from 'moment'; import rison from 'rison'; @@ -28,7 +34,6 @@ import ConfirmStatusChange from 'src/components/ConfirmStatusChange'; import DeleteModal from 'src/components/DeleteModal'; import ListView, { ListViewProps } from 'src/components/ListView'; import SubMenu, { SubMenuProps } from 'src/features/home/SubMenu'; -import { getClientErrorObject } from 'src/utils/getClientErrorObject'; import withToasts from 'src/components/MessageToasts/withToasts'; import { useListViewResource } from 'src/views/CRUD/hooks'; import { createErrorHandler } from 'src/views/CRUD/utils'; diff --git a/superset-frontend/src/pages/Chart/index.tsx b/superset-frontend/src/pages/Chart/index.tsx index c679e0e628..8cf47f0e3f 100644 --- a/superset-frontend/src/pages/Chart/index.tsx +++ b/superset-frontend/src/pages/Chart/index.tsx @@ -26,12 +26,12 @@ import { makeApi, SharedLabelColorSource, t, + getClientErrorObject, } from '@superset-ui/core'; import Loading from 'src/components/Loading'; import { addDangerToast } from 'src/components/MessageToasts/actions'; import { getUrlParam } from 'src/utils/urlUtils'; import { URL_PARAMS } from 'src/constants'; -import { getClientErrorObject } from 'src/utils/getClientErrorObject'; import getFormDataWithExtraFilters from 'src/dashboard/util/charts/getFormDataWithExtraFilters'; import { getAppliedFilterValues } from 'src/dashboard/util/activeDashboardFilters'; import { getParsedExploreURLParams } from 'src/explore/exploreUtils/getParsedExploreURLParams'; diff --git a/superset-frontend/src/setup/setupApp.ts b/superset-frontend/src/setup/setupApp.ts index 227aad3a68..0f31024cda 100644 --- a/superset-frontend/src/setup/setupApp.ts +++ b/superset-frontend/src/setup/setupApp.ts @@ -18,11 +18,11 @@ */ /* eslint global-require: 0 */ import $ from 'jquery'; -import { SupersetClient } from '@superset-ui/core'; import { + SupersetClient, getClientErrorObject, ClientErrorObject, -} from 'src/utils/getClientErrorObject'; +} from '@superset-ui/core'; import setupErrorMessages from 'src/setup/setupErrorMessages'; // eslint-disable-next-line @typescript-eslint/no-unused-vars diff --git a/superset-frontend/src/setup/setupErrorMessages.ts b/superset-frontend/src/setup/setupErrorMessages.ts index f393a36f1c..4d731cb4e3 100644 --- a/superset-frontend/src/setup/setupErrorMessages.ts +++ b/superset-frontend/src/setup/setupErrorMessages.ts @@ -16,8 +16,8 @@ * specific language governing permissions and limitations * under the License. */ +import { ErrorTypeEnum } from '@superset-ui/core'; import getErrorMessageComponentRegistry from 'src/components/ErrorMessage/getErrorMessageComponentRegistry'; -import { ErrorTypeEnum } from 'src/components/ErrorMessage/types'; import TimeoutErrorMessage from 'src/components/ErrorMessage/TimeoutErrorMessage'; import DatabaseErrorMessage from 'src/components/ErrorMessage/DatabaseErrorMessage'; import MarshmallowErrorMessage from 'src/components/ErrorMessage/MarshmallowErrorMessage'; diff --git a/superset-frontend/src/utils/errorMessages.ts b/superset-frontend/src/utils/errorMessages.ts deleted file mode 100644 index d5bfbdc17b..0000000000 --- a/superset-frontend/src/utils/errorMessages.ts +++ /dev/null @@ -1,26 +0,0 @@ -/** - * Licensed to the Apache Software Foundation (ASF) under one - * or more contributor license agreements. See the NOTICE file - * distributed with this work for additional information - * regarding copyright ownership. The ASF licenses this file - * to you under the Apache License, Version 2.0 (the - * "License"); you may not use this file except in compliance - * with the License. You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. - */ - -// Error messages used in many places across applications -const COMMON_ERR_MESSAGES = { - SESSION_TIMED_OUT: - 'Your session timed out, please refresh your page and try again.', -}; - -export default COMMON_ERR_MESSAGES; diff --git a/superset-frontend/src/utils/getClientErrorObject.test.ts b/superset-frontend/src/utils/getClientErrorObject.test.ts deleted file mode 100644 index 8e89fec284..0000000000 --- a/superset-frontend/src/utils/getClientErrorObject.test.ts +++ /dev/null @@ -1,83 +0,0 @@ -/** - * Licensed to the Apache Software Foundation (ASF) under one - * or more contributor license agreements. See the NOTICE file - * distributed with this work for additional information - * regarding copyright ownership. The ASF licenses this file - * to you under the Apache License, Version 2.0 (the - * "License"); you may not use this file except in compliance - * with the License. You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. - */ -import { ErrorTypeEnum } from 'src/components/ErrorMessage/types'; -import { getClientErrorObject } from 'src/utils/getClientErrorObject'; - -describe('getClientErrorObject()', () => { - it('Returns a Promise', () => { - const response = getClientErrorObject('error'); - expect(response instanceof Promise).toBe(true); - }); - - it('Returns a Promise that resolves to an object with an error key', () => { - const error = 'error'; - - return getClientErrorObject(error).then(errorObj => { - expect(errorObj).toMatchObject({ error }); - }); - }); - - it('Handles Response that can be parsed as json', () => { - const jsonError = { something: 'something', error: 'Error message' }; - const jsonErrorString = JSON.stringify(jsonError); - - return getClientErrorObject(new Response(jsonErrorString)).then( - errorObj => { - expect(errorObj).toMatchObject(jsonError); - }, - ); - }); - - it('Handles backwards compatibility between old error messages and the new SIP-40 errors format', () => { - const jsonError = { - errors: [ - { - error_type: ErrorTypeEnum.GENERIC_DB_ENGINE_ERROR, - extra: { engine: 'presto', link: 'https://www.google.com' }, - level: 'error', - message: 'presto error: test error', - }, - ], - }; - const jsonErrorString = JSON.stringify(jsonError); - - return getClientErrorObject(new Response(jsonErrorString)).then( - errorObj => { - expect(errorObj.error).toEqual(jsonError.errors[0].message); - expect(errorObj.link).toEqual(jsonError.errors[0].extra.link); - }, - ); - }); - - it('Handles Response that can be parsed as text', () => { - const textError = 'Hello I am a text error'; - - return getClientErrorObject(new Response(textError)).then(errorObj => { - expect(errorObj).toMatchObject({ error: textError }); - }); - }); - - it('Handles plain text as input', () => { - const error = 'error'; - - return getClientErrorObject(error).then(errorObj => { - expect(errorObj).toMatchObject({ error }); - }); - }); -}); diff --git a/superset-frontend/src/views/CRUD/hooks.ts b/superset-frontend/src/views/CRUD/hooks.ts index 8f31f2fcdd..129734fc8b 100644 --- a/superset-frontend/src/views/CRUD/hooks.ts +++ b/superset-frontend/src/views/CRUD/hooks.ts @@ -18,7 +18,13 @@ */ import rison from 'rison'; import { useState, useEffect, useCallback } from 'react'; -import { makeApi, SupersetClient, t, JsonObject } from '@superset-ui/core'; +import { + makeApi, + SupersetClient, + t, + JsonObject, + getClientErrorObject, +} from '@superset-ui/core'; import { createErrorHandler, @@ -33,7 +39,6 @@ import { FetchDataConfig } from 'src/components/ListView'; import { FilterValue } from 'src/components/ListView/types'; import Chart, { Slice } from 'src/types/Chart'; import copyTextToClipboard from 'src/utils/copy'; -import { getClientErrorObject } from 'src/utils/getClientErrorObject'; import SupersetText from 'src/utils/textUtils'; import { DatabaseObject } from 'src/features/databases/types'; import { FavoriteStatus, ImportResourceName } from './types'; diff --git a/superset-frontend/src/views/CRUD/utils.tsx b/superset-frontend/src/views/CRUD/utils.tsx index 514a39964e..96b8b0161d 100644 --- a/superset-frontend/src/views/CRUD/utils.tsx +++ b/superset-frontend/src/views/CRUD/utils.tsx @@ -24,12 +24,12 @@ import { SupersetClient, SupersetClientResponse, SupersetTheme, + getClientErrorObject, t, } from '@superset-ui/core'; import Chart from 'src/types/Chart'; import { intersection } from 'lodash'; import rison from 'rison'; -import { getClientErrorObject } from 'src/utils/getClientErrorObject'; import { FetchDataConfig, FilterValue } from 'src/components/ListView'; import SupersetText from 'src/utils/textUtils'; import { findPermission } from 'src/utils/findPermission'; diff --git a/superset/errors.py b/superset/errors.py index b792a120da..612977ad65 100644 --- a/superset/errors.py +++ b/superset/errors.py @@ -26,7 +26,7 @@ class SupersetErrorType(StrEnum): """ Types of errors that can exist within Superset. - Keep in sync with superset-frontend/src/components/ErrorMessage/types.ts + Keep in sync with superset-frontend/packages/superset-ui-core/src/query/types/Query.ts """ # Frontend errors @@ -196,7 +196,7 @@ class ErrorLevel(StrEnum): """ Levels of errors that can exist within Superset. - Keep in sync with superset-frontend/src/components/ErrorMessage/types.ts + Keep in sync with superset-frontend/packages/superset-ui-core/src/query/types/Query.ts """ INFO = "info"