refactor: Move fetchTimeRange to core package (#27852)

This commit is contained in:
Kamil Gabryjelski 2024-04-03 18:34:23 +02:00 committed by GitHub
parent 30bc8f06dc
commit a498d6d10f
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
72 changed files with 623 additions and 430 deletions

View File

@ -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.',
};

View File

@ -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';

View File

@ -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,

View File

@ -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';

View File

@ -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> = T[keyof T];
export type ErrorType = ValueOf<typeof ErrorTypeEnum>;
// 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<ExtraType = Record<string, any> | null> = {
error_type: ErrorType;

View File

@ -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,
};
}
};

View File

@ -21,3 +21,4 @@ export * from './types';
export { default as getComparisonInfo } from './getComparisonInfo';
export { default as getComparisonFilters } from './getComparisonFilters';
export { SEPARATOR, fetchTimeRange } from './fetchTimeRange';

View File

@ -49,14 +49,13 @@ const mockLoadQueryData = jest.fn<Promise<unknown>, 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<ChartClient>;

View File

@ -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();
});
});

View File

@ -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.');
});

View File

@ -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',
});
});

View File

@ -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';

View File

@ -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 = {

View File

@ -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;

View File

@ -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 {

View File

@ -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;

View File

@ -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';

View File

@ -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';

View File

@ -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';

View File

@ -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'));

View File

@ -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',

View File

@ -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;

View File

@ -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',

View File

@ -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',

View File

@ -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',

View File

@ -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 }>`

View File

@ -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',

View File

@ -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');

View File

@ -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', () => {

View File

@ -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';

View File

@ -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',

View File

@ -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',

View File

@ -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> = T[keyof T];
export type ErrorType = ValueOf<typeof ErrorTypeEnum>;
// Keep in sync with superset/views/errors.py
export type ErrorLevel = 'info' | 'warning' | 'error';
export type ErrorSource = 'dashboard' | 'explore' | 'sqllab' | 'crud';
export type SupersetError<ExtraType = Record<string, any> | 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<ExtraType = Record<string, any> | null> =
{
error: SupersetError<ExtraType>;
source?: ErrorSource;
subtitle?: React.ReactNode;
subtitle?: ReactNode;
};
export type ErrorMessageComponent =
React.ComponentType<ErrorMessageComponentProps>;
export type ErrorMessageComponent = ComponentType<ErrorMessageComponentProps>;

View File

@ -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,

View File

@ -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;

View File

@ -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<string, any>();

View File

@ -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,

View File

@ -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,

View File

@ -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';

View File

@ -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 {

View File

@ -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;

View File

@ -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,

View File

@ -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(<ColumnSelect {...(props as any)} />, {

View File

@ -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';

View File

@ -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,

View File

@ -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,

View File

@ -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 {

View File

@ -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';

View File

@ -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';

View File

@ -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 {

View File

@ -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,

View File

@ -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';

View File

@ -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',
);
});
});

View File

@ -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(

View File

@ -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';
/**

View File

@ -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({

View File

@ -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';

View File

@ -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<SelectProps, 'options' | 'ariaLabel' | 'onChange'>;

View File

@ -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';

View File

@ -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,

View File

@ -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,

View File

@ -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);

View File

@ -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;

View File

@ -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';

View File

@ -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';

View File

@ -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

View File

@ -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';

View File

@ -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;

View File

@ -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 });
});
});
});

View File

@ -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';

View File

@ -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';

View File

@ -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"