Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
19 commits
Select commit Hold shift + click to select a range
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
42 changes: 42 additions & 0 deletions docs/docs/configuration/databases.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -1769,6 +1769,48 @@ You can use the `Extra` field in the **Edit Databases** form to configure SSL:
}
}
```
##### Custom Error Messages
You can use the `CUSTOM_DATABASE_ERRORS` in the `superset/custom_database_errors.py` file or overwrite it in your config file to configure custom error messages for database exceptions.

This feature lets you transform raw database errors into user-friendly messages, optionally including documentation links and hiding default error codes.

Provide an empty string as the first value to keep the original error message. This way, you can add just a link to the documentation
**Example usage:**
```Python
CUSTOM_DATABASE_ERRORS = {
"database_name": {
re.compile('permission denied for view'): (
__(
'Permission denied'
),
SupersetErrorType.GENERIC_DB_ENGINE_ERROR,
{
"custom_doc_links": [
{
"url": "https://example.com/docs/1",
"label": "Check documentation"
},
],
"show_issue_info": False,
}
)
},
"examples": {
re.compile(r'message="(?P<message>[^"]*)"'): (
__(
'Unexpected error: "%(message)s"'
),
SupersetErrorType.GENERIC_DB_ENGINE_ERROR,
{}
)
}
}
```

**Options:**

- ``custom_doc_links``: List of documentation links to display with the error.
- ``show_issue_info``: Set to ``False`` to hide default error codes.

## Misc

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
/**
* 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 { render, screen } from 'spec/helpers/testing-library';
import { CustomDocLink } from './CustomDocLink';

const mockedProps = {
url: 'https://superset.apache.org/docs/',
label: 'Superset Docs',
};

test('should render the label', () => {
render(<CustomDocLink {...mockedProps} />);
expect(screen.getByText('Superset Docs')).toBeInTheDocument();
});

test('should render the link with correct attributes', () => {
render(<CustomDocLink {...mockedProps} />);
const link = screen.getByRole('link');
expect(link).toHaveAttribute('href', mockedProps.url);
expect(link).toHaveAttribute('target', '_blank');
expect(link).toHaveAttribute('rel', 'noopener noreferrer');
});
33 changes: 33 additions & 0 deletions superset-frontend/src/components/ErrorMessage/CustomDocLink.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
/**
* 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 { Flex, Icons } from '@superset-ui/core/components';

export type CustomDocLinkProps = {
url: string;
label: string;
};

export const CustomDocLink = ({ url, label }: CustomDocLinkProps) => (
<a href={url} target="_blank" rel="noopener noreferrer">
<Flex align="center" gap={4}>
{label} <Icons.Full iconSize="m" />
</Flex>
</a>
);
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,38 @@ const mockedProps = {
subtitle: 'Error message',
};

const mockedPropsWithCustomError = {
...mockedProps,
error: {
...mockedProps.error,
extra: {
...mockedProps.error.extra,
custom_doc_links: [
{
label: 'Custom Doc Link 1',
url: 'https://example.com/custom-doc-1',
},
{
label: 'Custom Doc Link 2',
url: 'https://example.com/custom-doc-2',
},
],
show_issue_info: false,
},
},
};

const mockedPropsWithCustomErrorAndBadLinks = {
...mockedProps,
error: {
...mockedProps.error,
extra: {
...mockedProps.error.extra,
custom_doc_links: true,
},
},
};

test('should render', () => {
const nullExtraProps = {
...mockedProps,
Expand Down Expand Up @@ -112,3 +144,28 @@ test('should NOT render the owners', () => {
screen.queryByText('Chart Owners: Owner A, Owner B'),
).not.toBeInTheDocument();
});

test('should render custom documentation links when provided', () => {
render(<DatabaseErrorMessage {...mockedPropsWithCustomError} />, {
useRedux: true,
});
expect(screen.getByText('Custom Doc Link 1')).toBeInTheDocument();
expect(screen.getByText('Custom Doc Link 2')).toBeInTheDocument();
});

test('should NOT render see more button when show_issue_info is false', () => {
render(<DatabaseErrorMessage {...mockedPropsWithCustomError} />, {
useRedux: true,
});
const button = screen.queryByText('See more');
expect(button).not.toBeInTheDocument();
});

test('should render message when wrong value provided for custom_doc_urls', () => {
// @ts-ignore
render(<DatabaseErrorMessage {...mockedPropsWithCustomErrorAndBadLinks} />, {
useRedux: true,
});
const button = screen.queryByText('Error message');
expect(button).toBeInTheDocument();
});
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ import { t, tn } from '@superset-ui/core';
import type { ErrorMessageComponentProps } from './types';
import { IssueCode } from './IssueCode';
import { ErrorAlert } from './ErrorAlert';
import { CustomDocLink, CustomDocLinkProps } from './CustomDocLink';

interface DatabaseErrorExtra {
owners?: string[];
Expand All @@ -30,6 +31,8 @@ interface DatabaseErrorExtra {
message: string;
}[];
engine_name: string | null;
custom_doc_links?: CustomDocLinkProps[];
show_issue_info?: boolean;
}

export function DatabaseErrorMessage({
Expand All @@ -40,20 +43,32 @@ export function DatabaseErrorMessage({

const isVisualization = ['dashboard', 'explore'].includes(source || '');
const [firstLine, ...remainingLines] = message.split('\n');
const alertMessage = firstLine;
const alertDescription =
remainingLines.length > 0 ? remainingLines.join('\n') : null;
let alertMessage: ReactNode = firstLine;

const body = extra && (
if (Array.isArray(extra?.custom_doc_links)) {
alertMessage = (
<>
{firstLine}
{extra.custom_doc_links.map(link => (
<div key={link.url}>
<CustomDocLink {...link} />
</div>
))}
</>
);
}

const body = extra && extra.show_issue_info !== false && (
<>
<p>
{t('This may be triggered by:')}
<br />
{extra.issue_codes
?.map<ReactNode>(issueCode => (
<IssueCode {...issueCode} key={issueCode.code} />
))
.reduce((prev, curr) => [prev, <br />, curr])}
{extra.issue_codes?.flatMap((issueCode, idx, arr) => [

This comment was marked as resolved.

<IssueCode {...issueCode} key={issueCode.code} />,
idx < arr.length - 1 ? <br key={`br-${issueCode.code}`} /> : null,
])}
</p>
{isVisualization && extra.owners && (
<>
Expand Down
4 changes: 3 additions & 1 deletion superset/commands/database/test_connection.py
Original file line number Diff line number Diff line change
Expand Up @@ -189,7 +189,9 @@ def run( # noqa: C901
engine=database.db_engine_spec.__name__,
)
# check for custom errors (wrong username, wrong password, etc)
errors = database.db_engine_spec.extract_errors(ex, self._context)
errors = database.db_engine_spec.extract_errors(
ex, self._context, database_name=database.unique_name
)
raise SupersetErrorsException(errors, status=400) from ex
except OAuth2RedirectError:
raise
Expand Down
4 changes: 3 additions & 1 deletion superset/commands/database/validate.py
Original file line number Diff line number Diff line change
Expand Up @@ -124,7 +124,9 @@ def run(self) -> None:
"username": url.username,
"database": url.database,
}
errors = database.db_engine_spec.extract_errors(ex, context)
errors = database.db_engine_spec.extract_errors(
ex, context, database_name=database.unique_name
)
raise DatabaseTestConnectionFailedError(errors, status=400) from ex

if not alive:
Expand Down
8 changes: 8 additions & 0 deletions superset/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -2172,6 +2172,14 @@ class ExtraDynamicQueryFilters(TypedDict, total=False):
# keeping a web API call open for this long.
SYNC_DB_PERMISSIONS_IN_ASYNC_MODE: bool = False

# CUSTOM_DATABASE_ERRORS: Configure custom error messages for database exceptions
# in superset/custom_database_errors.py.
# Transform raw database errors into user-friendly messages with optional documentation
try:
from superset.custom_database_errors import CUSTOM_DATABASE_ERRORS
except ImportError:
CUSTOM_DATABASE_ERRORS = {}


LOCAL_EXTENSIONS: list[str] = []
EXTENSIONS_PATH: str | None = None
Expand Down
5 changes: 4 additions & 1 deletion superset/connectors/sqla/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -1642,7 +1642,10 @@ def assign_column_label(df: pd.DataFrame) -> pd.DataFrame | None:
)
db_engine_spec = self.db_engine_spec
errors = [
dataclasses.asdict(error) for error in db_engine_spec.extract_errors(ex)
dataclasses.asdict(error)
for error in db_engine_spec.extract_errors(
ex, database_name=self.database.unique_name
)
]
error_message = utils.error_msg_from_exception(ex)

Expand Down
83 changes: 83 additions & 0 deletions superset/custom_database_errors.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
# 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 re
from typing import Any

from flask_babel import gettext as __

from superset.errors import SupersetErrorType

# CUSTOM_DATABASE_ERRORS: Configure custom error messages for database exceptions.
# Transform raw database errors into user-friendly messages with optional documentation
# links using custom_doc_links. Set show_issue_info=False to hide default error codes.
# Example:
# CUSTOM_DATABASE_ERRORS = {
# "database_name": {
# re.compile('permission denied for view'): (
# __(
# 'Permission denied'
# ),
# SupersetErrorType.GENERIC_DB_ENGINE_ERROR,
# {
# "custom_doc_links": [
# {
# "url": "https://example.com/docs/1",
# "label": "Check documentation"
# },
# ],
# "show_issue_info": False,
# }
# )
# },
# "examples": {
# re.compile(r'message="(?P<message>[^"]*)"'): (
# __(
# 'Unexpected error: "%(message)s"'
# ),
# SupersetErrorType.GENERIC_DB_ENGINE_ERROR,
# {}
# )
# }
# }

CUSTOM_DATABASE_ERRORS: dict[
str, dict[re.Pattern[str], tuple[str, SupersetErrorType, dict[str, Any]]]
] = {
"examples": {
re.compile("no such table: a"): (
__("This is custom error message for a"),
SupersetErrorType.GENERIC_DB_ENGINE_ERROR,
{
"custom_doc_links": [
{
"url": "https://example.com/docs/1",
"label": "Custom documentation link",
},
],
"show_issue_info": False,
},
),
re.compile("no such table: b"): (
__("This is custom error message for b"),
SupersetErrorType.GENERIC_DB_ENGINE_ERROR,
{
"show_issue_info": True,
},
),
}
}
Loading
Loading