feat: update timeout error UX (#10274)
This commit is contained in:
parent
d92cb66f60
commit
5fa4680447
|
|
@ -164,6 +164,7 @@ Contents
|
||||||
gallery
|
gallery
|
||||||
druid
|
druid
|
||||||
misc
|
misc
|
||||||
|
issue_code_reference
|
||||||
faq
|
faq
|
||||||
|
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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.
|
||||||
|
|
||||||
|
Issue Code Reference
|
||||||
|
====================
|
||||||
|
|
||||||
|
This page lists issue codes that may be displayed in Superset and provides additional context.
|
||||||
|
|
||||||
|
Issue 1000
|
||||||
|
""""""""""
|
||||||
|
|
||||||
|
.. code-block:: text
|
||||||
|
|
||||||
|
The datasource is too large to query.
|
||||||
|
|
||||||
|
It's likely your datasource has grown too large to run the current query, and is timing out. You can resolve this by reducing the size of your datasource or by modifying your query to only process a subset of your data.
|
||||||
|
|
||||||
|
Issue 1001
|
||||||
|
""""""""""
|
||||||
|
|
||||||
|
.. code-block:: text
|
||||||
|
|
||||||
|
The database is under an unusual load.
|
||||||
|
|
||||||
|
Your query may have timed out because of unusually high load on the database engine. You can make your query simpler, or wait until the database is under less load and try again.
|
||||||
|
|
@ -0,0 +1,21 @@
|
||||||
|
<!--
|
||||||
|
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.
|
||||||
|
-->
|
||||||
|
<svg width="18" height="18" viewBox="0 0 18 18" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<path fill-rule="evenodd" clip-rule="evenodd" d="M10.4099 9L16.7099 2.71C17.1021 2.31788 17.1021 1.68212 16.7099 1.29C16.3178 0.89788 15.6821 0.89788 15.2899 1.29L8.99994 7.59L2.70994 1.29C2.31782 0.89788 1.68206 0.89788 1.28994 1.29C0.897817 1.68212 0.897817 2.31788 1.28994 2.71L7.58994 9L1.28994 15.29C1.10063 15.4778 0.994141 15.7334 0.994141 16C0.994141 16.2666 1.10063 16.5222 1.28994 16.71C1.47771 16.8993 1.7333 17.0058 1.99994 17.0058C2.26658 17.0058 2.52217 16.8993 2.70994 16.71L8.99994 10.41L15.2899 16.71C15.4777 16.8993 15.7333 17.0058 15.9999 17.0058C16.2666 17.0058 16.5222 16.8993 16.7099 16.71C16.8993 16.5222 17.0057 16.2666 17.0057 16C17.0057 15.7334 16.8993 15.4778 16.7099 15.29L10.4099 9Z" fill="currentColor"/>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 1.6 KiB |
|
|
@ -156,7 +156,7 @@ describe('chart actions', () => {
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should CHART_UPDATE_TIMEOUT action upon query timeout', () => {
|
it('should dispatch CHART_UPDATE_FAILED action upon query timeout', () => {
|
||||||
const unresolvingPromise = new Promise(() => {});
|
const unresolvingPromise = new Promise(() => {});
|
||||||
fetchMock.post(MOCK_URL, () => unresolvingPromise, {
|
fetchMock.post(MOCK_URL, () => unresolvingPromise, {
|
||||||
overwriteRoutes: true,
|
overwriteRoutes: true,
|
||||||
|
|
@ -169,7 +169,7 @@ describe('chart actions', () => {
|
||||||
// chart update, trigger query, update form data, fail
|
// chart update, trigger query, update form data, fail
|
||||||
expect(fetchMock.calls(MOCK_URL)).toHaveLength(1);
|
expect(fetchMock.calls(MOCK_URL)).toHaveLength(1);
|
||||||
expect(dispatch.callCount).toBe(5);
|
expect(dispatch.callCount).toBe(5);
|
||||||
expect(dispatch.args[4][0].type).toBe(actions.CHART_UPDATE_TIMEOUT);
|
expect(dispatch.args[4][0].type).toBe(actions.CHART_UPDATE_FAILED);
|
||||||
setupDefaultFetchMock();
|
setupDefaultFetchMock();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -40,7 +40,21 @@ describe('chart reducers', () => {
|
||||||
it('should update endtime on timeout', () => {
|
it('should update endtime on timeout', () => {
|
||||||
const newState = chartReducer(
|
const newState = chartReducer(
|
||||||
charts,
|
charts,
|
||||||
actions.chartUpdateTimeout('timeout', 60, chartKey),
|
actions.chartUpdateFailed(
|
||||||
|
{
|
||||||
|
statusText: 'timeout',
|
||||||
|
error: 'Request timed out',
|
||||||
|
errors: [
|
||||||
|
{
|
||||||
|
error_type: 'FRONTEND_TIMEOUT_ERROR',
|
||||||
|
extra: { timeout: 1 },
|
||||||
|
level: 'error',
|
||||||
|
message: 'Request timed out',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
chartKey,
|
||||||
|
),
|
||||||
);
|
);
|
||||||
expect(newState[chartKey].chartUpdateEndTime).toBeGreaterThan(0);
|
expect(newState[chartKey].chartUpdateEndTime).toBeGreaterThan(0);
|
||||||
expect(newState[chartKey].chartStatus).toEqual('failed');
|
expect(newState[chartKey].chartStatus).toEqual('failed');
|
||||||
|
|
|
||||||
|
|
@ -217,11 +217,14 @@ export default class ResultSet extends React.PureComponent<
|
||||||
return <Alert bsStyle="warning">Query was stopped</Alert>;
|
return <Alert bsStyle="warning">Query was stopped</Alert>;
|
||||||
} else if (query.state === 'failed') {
|
} else if (query.state === 'failed') {
|
||||||
return (
|
return (
|
||||||
<ErrorMessageWithStackTrace
|
<div className="result-set-error-message">
|
||||||
error={query.errors?.[0]}
|
<ErrorMessageWithStackTrace
|
||||||
message={query.errorMessage || undefined}
|
error={query?.errors?.[0]}
|
||||||
link={query.link}
|
message={query.errorMessage || undefined}
|
||||||
/>
|
link={query.link}
|
||||||
|
source="sqllab"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
);
|
);
|
||||||
} else if (query.state === 'success' && query.ctas) {
|
} else if (query.state === 'success' && query.ctas) {
|
||||||
const { tempSchema, tempTable } = query;
|
const { tempSchema, tempTable } = query;
|
||||||
|
|
|
||||||
|
|
@ -409,6 +409,10 @@ div.tablePopover {
|
||||||
padding-right: 8px;
|
padding-right: 8px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.result-set-error-message {
|
||||||
|
padding-top: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
.filterable-table-container {
|
.filterable-table-container {
|
||||||
margin-top: 48px;
|
margin-top: 48px;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -49,6 +49,7 @@ const propTypes = {
|
||||||
timeout: PropTypes.number,
|
timeout: PropTypes.number,
|
||||||
vizType: PropTypes.string.isRequired,
|
vizType: PropTypes.string.isRequired,
|
||||||
triggerRender: PropTypes.bool,
|
triggerRender: PropTypes.bool,
|
||||||
|
owners: PropTypes.arrayOf(PropTypes.string),
|
||||||
// state
|
// state
|
||||||
chartAlert: PropTypes.string,
|
chartAlert: PropTypes.string,
|
||||||
chartStatus: PropTypes.string,
|
chartStatus: PropTypes.string,
|
||||||
|
|
@ -139,12 +140,26 @@ class Chart extends React.PureComponent {
|
||||||
}
|
}
|
||||||
|
|
||||||
renderErrorMessage() {
|
renderErrorMessage() {
|
||||||
const { chartAlert, chartStackTrace, queryResponse } = this.props;
|
const {
|
||||||
|
chartAlert,
|
||||||
|
chartStackTrace,
|
||||||
|
dashboardId,
|
||||||
|
owners,
|
||||||
|
queryResponse,
|
||||||
|
} = this.props;
|
||||||
|
|
||||||
|
const error = queryResponse?.errors?.[0];
|
||||||
|
if (error) {
|
||||||
|
const extra = error.extra || {};
|
||||||
|
extra.owners = owners;
|
||||||
|
error.extra = extra;
|
||||||
|
}
|
||||||
return (
|
return (
|
||||||
<ErrorMessageWithStackTrace
|
<ErrorMessageWithStackTrace
|
||||||
error={queryResponse?.errors?.[0]}
|
error={error}
|
||||||
message={chartAlert || queryResponse?.message}
|
message={chartAlert || queryResponse?.message}
|
||||||
link={queryResponse ? queryResponse.link : null}
|
link={queryResponse ? queryResponse.link : null}
|
||||||
|
source={dashboardId ? 'dashboard' : 'explore'}
|
||||||
stackTrace={chartStackTrace}
|
stackTrace={chartStackTrace}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
|
|
|
||||||
|
|
@ -62,11 +62,6 @@ export function chartUpdateStopped(key) {
|
||||||
return { type: CHART_UPDATE_STOPPED, key };
|
return { type: CHART_UPDATE_STOPPED, key };
|
||||||
}
|
}
|
||||||
|
|
||||||
export const CHART_UPDATE_TIMEOUT = 'CHART_UPDATE_TIMEOUT';
|
|
||||||
export function chartUpdateTimeout(statusText, timeout, key) {
|
|
||||||
return { type: CHART_UPDATE_TIMEOUT, statusText, timeout, key };
|
|
||||||
}
|
|
||||||
|
|
||||||
export const CHART_UPDATE_FAILED = 'CHART_UPDATE_FAILED';
|
export const CHART_UPDATE_FAILED = 'CHART_UPDATE_FAILED';
|
||||||
export function chartUpdateFailed(queryResponse, key) {
|
export function chartUpdateFailed(queryResponse, key) {
|
||||||
return { type: CHART_UPDATE_FAILED, queryResponse, key };
|
return { type: CHART_UPDATE_FAILED, queryResponse, key };
|
||||||
|
|
@ -391,19 +386,16 @@ export function exploreJSON(
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
if (response.name === 'AbortError') {
|
||||||
if (response.statusText === 'timeout') {
|
|
||||||
appendErrorLog('timeout');
|
|
||||||
return dispatch(
|
|
||||||
chartUpdateTimeout(response.statusText, timeout, key),
|
|
||||||
);
|
|
||||||
} else if (response.name === 'AbortError') {
|
|
||||||
appendErrorLog('abort');
|
appendErrorLog('abort');
|
||||||
return dispatch(chartUpdateStopped(key));
|
return dispatch(chartUpdateStopped(key));
|
||||||
}
|
}
|
||||||
return getClientErrorObject(response).then(parsedResponse => {
|
return getClientErrorObject(response).then(parsedResponse => {
|
||||||
// query is processed, but error out.
|
if (response.statusText === 'timeout') {
|
||||||
appendErrorLog(parsedResponse.error, parsedResponse.is_cached);
|
appendErrorLog('timeout');
|
||||||
|
} else {
|
||||||
|
appendErrorLog(parsedResponse.error, parsedResponse.is_cached);
|
||||||
|
}
|
||||||
return dispatch(chartUpdateFailed(parsedResponse, key));
|
return dispatch(chartUpdateFailed(parsedResponse, key));
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -85,21 +85,6 @@ export default function chartReducer(charts = {}, action) {
|
||||||
),
|
),
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
[actions.CHART_UPDATE_TIMEOUT](state) {
|
|
||||||
return {
|
|
||||||
...state,
|
|
||||||
chartStatus: 'failed',
|
|
||||||
chartAlert: `${t('Query timeout')} - ${t(
|
|
||||||
`visualization queries are set to timeout at ${action.timeout} seconds. `,
|
|
||||||
)}${t(
|
|
||||||
'Perhaps your data has grown, your database is under unusual load, ' +
|
|
||||||
'or you are simply querying a data source that is too large ' +
|
|
||||||
'to be processed within the timeout range. ' +
|
|
||||||
'If that is the case, we recommend that you summarize your data further.',
|
|
||||||
)}`,
|
|
||||||
chartUpdateEndTime: now(),
|
|
||||||
};
|
|
||||||
},
|
|
||||||
[actions.CHART_UPDATE_FAILED](state) {
|
[actions.CHART_UPDATE_FAILED](state) {
|
||||||
return {
|
return {
|
||||||
...state,
|
...state,
|
||||||
|
|
|
||||||
|
|
@ -22,13 +22,14 @@ import { Alert, Collapse } from 'react-bootstrap';
|
||||||
import { t } from '@superset-ui/translation';
|
import { t } from '@superset-ui/translation';
|
||||||
|
|
||||||
import getErrorMessageComponentRegistry from './getErrorMessageComponentRegistry';
|
import getErrorMessageComponentRegistry from './getErrorMessageComponentRegistry';
|
||||||
import { SupersetError } from './types';
|
import { SupersetError, ErrorSource } from './types';
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
error?: SupersetError;
|
error?: SupersetError;
|
||||||
link?: string;
|
link?: string;
|
||||||
message?: string;
|
message?: string;
|
||||||
stackTrace?: string;
|
stackTrace?: string;
|
||||||
|
source?: ErrorSource;
|
||||||
};
|
};
|
||||||
|
|
||||||
export default function ErrorMessageWithStackTrace({
|
export default function ErrorMessageWithStackTrace({
|
||||||
|
|
@ -36,6 +37,7 @@ export default function ErrorMessageWithStackTrace({
|
||||||
message,
|
message,
|
||||||
link,
|
link,
|
||||||
stackTrace,
|
stackTrace,
|
||||||
|
source,
|
||||||
}: Props) {
|
}: Props) {
|
||||||
const [showStackTrace, setShowStackTrace] = useState(false);
|
const [showStackTrace, setShowStackTrace] = useState(false);
|
||||||
|
|
||||||
|
|
@ -45,7 +47,7 @@ export default function ErrorMessageWithStackTrace({
|
||||||
error.error_type,
|
error.error_type,
|
||||||
);
|
);
|
||||||
if (ErrorMessageComponent) {
|
if (ErrorMessageComponent) {
|
||||||
return <ErrorMessageComponent error={error} />;
|
return <ErrorMessageComponent error={error} source={source} />;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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 React from 'react';
|
||||||
|
|
||||||
|
interface IssueCodeProps {
|
||||||
|
code: number;
|
||||||
|
message: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function IssueCode({ code, message }: IssueCodeProps) {
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
{message}{' '}
|
||||||
|
<a
|
||||||
|
href={`https://superset.apache.org/issue_code_reference.html#issue-${code}`}
|
||||||
|
rel="noopener noreferrer"
|
||||||
|
target="_blank"
|
||||||
|
>
|
||||||
|
<i className="fa fa-external-link" />
|
||||||
|
</a>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,248 @@
|
||||||
|
/**
|
||||||
|
* 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 React, { useState } from 'react';
|
||||||
|
import { Modal } from 'react-bootstrap';
|
||||||
|
import { styled, supersetTheme } from '@superset-ui/style';
|
||||||
|
import { t, tn } from '@superset-ui/translation';
|
||||||
|
|
||||||
|
import { noOp } from 'src/utils/common';
|
||||||
|
import Icon from '../Icon';
|
||||||
|
import Button from '../../views/datasetList/Button';
|
||||||
|
import { ErrorMessageComponentProps } from './types';
|
||||||
|
import CopyToClipboard from '../CopyToClipboard';
|
||||||
|
import IssueCode from './IssueCode';
|
||||||
|
|
||||||
|
const ErrorAlert = styled.div`
|
||||||
|
align-items: center;
|
||||||
|
background-color: ${({ theme }) => theme.colors.error.light2};
|
||||||
|
border-radius: ${({ theme }) => theme.borderRadius}px;
|
||||||
|
border: 1px solid ${({ theme }) => theme.colors.error.base};
|
||||||
|
color: ${({ theme }) => theme.colors.error.dark2};
|
||||||
|
padding: ${({ theme }) => 2 * theme.gridUnit}px;
|
||||||
|
width: 100%;
|
||||||
|
|
||||||
|
.top-row {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
}
|
||||||
|
|
||||||
|
.error-body {
|
||||||
|
padding-top: ${({ theme }) => theme.gridUnit}px;
|
||||||
|
padding-left: ${({ theme }) => 8 * theme.gridUnit}px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.icon {
|
||||||
|
margin-right: ${({ theme }) => 2 * theme.gridUnit}px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.link {
|
||||||
|
color: ${({ theme }) => theme.colors.error.dark2};
|
||||||
|
text-decoration: underline;
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
|
||||||
|
const ErrorModal = styled(Modal)`
|
||||||
|
color: ${({ theme }) => theme.colors.error.dark2};
|
||||||
|
|
||||||
|
.icon {
|
||||||
|
margin-right: ${({ theme }) => 2 * theme.gridUnit}px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.header {
|
||||||
|
align-items: center;
|
||||||
|
background-color: ${({ theme }) => theme.colors.error.light2};
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
font-size: ${({ theme }) => theme.typography.sizes.l}px;
|
||||||
|
|
||||||
|
// Remove clearfix hack as Superset is only used on modern browsers
|
||||||
|
::before,
|
||||||
|
::after {
|
||||||
|
content: unset;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
|
||||||
|
const LeftSideContent = styled.div`
|
||||||
|
align-items: center;
|
||||||
|
display: flex;
|
||||||
|
`;
|
||||||
|
|
||||||
|
interface TimeoutErrorExtra {
|
||||||
|
issue_codes: {
|
||||||
|
code: number;
|
||||||
|
message: string;
|
||||||
|
}[];
|
||||||
|
owners?: string[];
|
||||||
|
timeout: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
function TimeoutErrorMessage({
|
||||||
|
error,
|
||||||
|
source,
|
||||||
|
}: ErrorMessageComponentProps<TimeoutErrorExtra>) {
|
||||||
|
const [isModalOpen, setIsModalOpen] = useState(false);
|
||||||
|
const [isMessageExpanded, setIsMessageExpanded] = useState(false);
|
||||||
|
const { extra } = error;
|
||||||
|
|
||||||
|
const isVisualization = (['dashboard', 'explore'] as (
|
||||||
|
| string
|
||||||
|
| undefined
|
||||||
|
)[]).includes(source);
|
||||||
|
|
||||||
|
const isExpandable = (['explore', 'sqllab'] as (
|
||||||
|
| string
|
||||||
|
| undefined
|
||||||
|
)[]).includes(source);
|
||||||
|
|
||||||
|
const title = isVisualization
|
||||||
|
? tn(
|
||||||
|
'We’re having trouble loading this visualization. Queries are set to timeout after %s second.',
|
||||||
|
'We’re having trouble loading this visualization. Queries are set to timeout after %s seconds.',
|
||||||
|
extra.timeout,
|
||||||
|
extra.timeout,
|
||||||
|
)
|
||||||
|
: tn(
|
||||||
|
'We’re having trouble loading these results. Queries are set to timeout after %s second.',
|
||||||
|
'We’re having trouble loading these results. Queries are set to timeout after %s seconds.',
|
||||||
|
extra.timeout,
|
||||||
|
extra.timeout,
|
||||||
|
);
|
||||||
|
|
||||||
|
const message = (
|
||||||
|
<>
|
||||||
|
<p>
|
||||||
|
{t('This may be triggered by:')}
|
||||||
|
<br />
|
||||||
|
{extra.issue_codes
|
||||||
|
.map<React.ReactNode>(issueCode => <IssueCode {...issueCode} />)
|
||||||
|
.reduce((prev, curr) => [prev, <br />, curr])}
|
||||||
|
</p>
|
||||||
|
{isVisualization && extra.owners && (
|
||||||
|
<>
|
||||||
|
<br />
|
||||||
|
<p>
|
||||||
|
{tn(
|
||||||
|
'Please reach out to the Chart Owner for assistance.',
|
||||||
|
'Please reach out to the Chart Owners for assistance.',
|
||||||
|
extra.owners.length,
|
||||||
|
)}
|
||||||
|
</p>
|
||||||
|
<p>
|
||||||
|
{tn(
|
||||||
|
'Chart Owner: %s',
|
||||||
|
'Chart Owners: %s',
|
||||||
|
extra.owners.length,
|
||||||
|
extra.owners.join(', '),
|
||||||
|
)}
|
||||||
|
</p>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
|
||||||
|
const copyText = `${title}
|
||||||
|
${t('This may be triggered by:')}
|
||||||
|
${extra.issue_codes.map(issueCode => issueCode.message).join('\n')}`;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<ErrorAlert>
|
||||||
|
<div className="top-row">
|
||||||
|
<LeftSideContent>
|
||||||
|
<Icon
|
||||||
|
className="icon"
|
||||||
|
name="error"
|
||||||
|
color={supersetTheme.colors.error.base}
|
||||||
|
/>
|
||||||
|
<strong>{t('Timeout Error')}</strong>
|
||||||
|
</LeftSideContent>
|
||||||
|
{!isExpandable && (
|
||||||
|
<a href="#" className="link" onClick={() => setIsModalOpen(true)}>
|
||||||
|
{t('See More')}
|
||||||
|
</a>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
{isExpandable ? (
|
||||||
|
<div className="error-body">
|
||||||
|
<p>{title}</p>
|
||||||
|
{!isMessageExpanded && (
|
||||||
|
<a
|
||||||
|
href="#"
|
||||||
|
className="link"
|
||||||
|
onClick={() => setIsMessageExpanded(true)}
|
||||||
|
>
|
||||||
|
{t('See More')}
|
||||||
|
</a>
|
||||||
|
)}
|
||||||
|
{isMessageExpanded && (
|
||||||
|
<>
|
||||||
|
<br />
|
||||||
|
{message}
|
||||||
|
<a
|
||||||
|
href="#"
|
||||||
|
className="link"
|
||||||
|
onClick={() => setIsMessageExpanded(false)}
|
||||||
|
>
|
||||||
|
{t('See Less')}
|
||||||
|
</a>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<ErrorModal show={isModalOpen} onHide={() => setIsModalOpen(false)}>
|
||||||
|
<Modal.Header className="header">
|
||||||
|
<LeftSideContent>
|
||||||
|
<Icon
|
||||||
|
className="icon"
|
||||||
|
name="error"
|
||||||
|
color={supersetTheme.colors.error.base}
|
||||||
|
/>
|
||||||
|
<div className="title">{t('Timeout Error')}</div>
|
||||||
|
</LeftSideContent>
|
||||||
|
<span
|
||||||
|
role="button"
|
||||||
|
tabIndex={0}
|
||||||
|
onClick={() => setIsModalOpen(false)}
|
||||||
|
>
|
||||||
|
<Icon name="close" />
|
||||||
|
</span>
|
||||||
|
</Modal.Header>
|
||||||
|
<Modal.Body>
|
||||||
|
<p>{title}</p>
|
||||||
|
<br />
|
||||||
|
{message}
|
||||||
|
</Modal.Body>
|
||||||
|
<Modal.Footer>
|
||||||
|
<CopyToClipboard
|
||||||
|
text={copyText}
|
||||||
|
shouldShowText={false}
|
||||||
|
wrapped={false}
|
||||||
|
copyNode={<Button onClick={noOp}>{t('Copy Message')}</Button>}
|
||||||
|
/>
|
||||||
|
<Button bsStyle="primary" onClick={() => setIsModalOpen(false)}>
|
||||||
|
{t('Close')}
|
||||||
|
</Button>
|
||||||
|
</Modal.Footer>
|
||||||
|
</ErrorModal>
|
||||||
|
)}
|
||||||
|
</ErrorAlert>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default TimeoutErrorMessage;
|
||||||
|
|
@ -37,6 +37,9 @@ export const ErrorTypeEnum = {
|
||||||
TABLE_SECURITY_ACCESS_ERROR: 'TABLE_SECURITY_ACCESS_ERROR',
|
TABLE_SECURITY_ACCESS_ERROR: 'TABLE_SECURITY_ACCESS_ERROR',
|
||||||
DATASOURCE_SECURITY_ACCESS_ERROR: 'DATASOURCE_SECURITY_ACCESS_ERROR',
|
DATASOURCE_SECURITY_ACCESS_ERROR: 'DATASOURCE_SECURITY_ACCESS_ERROR',
|
||||||
MISSING_OWNERSHIP_ERROR: 'MISSING_OWNERSHIP_ERROR',
|
MISSING_OWNERSHIP_ERROR: 'MISSING_OWNERSHIP_ERROR',
|
||||||
|
|
||||||
|
// Other errors
|
||||||
|
BACKEND_TIMEOUT_ERROR: 'BACKEND_TIMEOUT_ERROR',
|
||||||
} as const;
|
} as const;
|
||||||
|
|
||||||
type ValueOf<T> = T[keyof T];
|
type ValueOf<T> = T[keyof T];
|
||||||
|
|
@ -46,15 +49,20 @@ export type ErrorType = ValueOf<typeof ErrorTypeEnum>;
|
||||||
// Keep in sync with superset/views/errors.py
|
// Keep in sync with superset/views/errors.py
|
||||||
export type ErrorLevel = 'info' | 'warning' | 'error';
|
export type ErrorLevel = 'info' | 'warning' | 'error';
|
||||||
|
|
||||||
export type SupersetError = {
|
export type ErrorSource = 'dashboard' | 'explore' | 'sqllab';
|
||||||
|
|
||||||
|
export type SupersetError<ExtraType = Record<string, any> | null> = {
|
||||||
error_type: ErrorType;
|
error_type: ErrorType;
|
||||||
extra: Record<string, any> | null;
|
extra: ExtraType;
|
||||||
level: ErrorLevel;
|
level: ErrorLevel;
|
||||||
message: string;
|
message: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type ErrorMessageComponentProps = {
|
export type ErrorMessageComponentProps<
|
||||||
error: SupersetError;
|
ExtraType = Record<string, any> | null
|
||||||
|
> = {
|
||||||
|
error: SupersetError<ExtraType>;
|
||||||
|
source?: ErrorSource;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type ErrorMessageComponent = React.ComponentType<
|
export type ErrorMessageComponent = React.ComponentType<
|
||||||
|
|
|
||||||
|
|
@ -17,12 +17,12 @@
|
||||||
* under the License.
|
* under the License.
|
||||||
*/
|
*/
|
||||||
import React, { SVGProps } from 'react';
|
import React, { SVGProps } from 'react';
|
||||||
import styled from '@superset-ui/style';
|
|
||||||
import { ReactComponent as CancelXIcon } from 'images/icons/cancel-x.svg';
|
import { ReactComponent as CancelXIcon } from 'images/icons/cancel-x.svg';
|
||||||
import { ReactComponent as CheckIcon } from 'images/icons/check.svg';
|
import { ReactComponent as CheckIcon } from 'images/icons/check.svg';
|
||||||
import { ReactComponent as CheckboxHalfIcon } from 'images/icons/checkbox-half.svg';
|
import { ReactComponent as CheckboxHalfIcon } from 'images/icons/checkbox-half.svg';
|
||||||
import { ReactComponent as CheckboxOffIcon } from 'images/icons/checkbox-off.svg';
|
import { ReactComponent as CheckboxOffIcon } from 'images/icons/checkbox-off.svg';
|
||||||
import { ReactComponent as CheckboxOnIcon } from 'images/icons/checkbox-on.svg';
|
import { ReactComponent as CheckboxOnIcon } from 'images/icons/checkbox-on.svg';
|
||||||
|
import { ReactComponent as CloseIcon } from 'images/icons/close.svg';
|
||||||
import { ReactComponent as CompassIcon } from 'images/icons/compass.svg';
|
import { ReactComponent as CompassIcon } from 'images/icons/compass.svg';
|
||||||
import { ReactComponent as DatasetPhysicalIcon } from 'images/icons/dataset_physical.svg';
|
import { ReactComponent as DatasetPhysicalIcon } from 'images/icons/dataset_physical.svg';
|
||||||
import { ReactComponent as DatasetVirtualIcon } from 'images/icons/dataset_virtual.svg';
|
import { ReactComponent as DatasetVirtualIcon } from 'images/icons/dataset_virtual.svg';
|
||||||
|
|
@ -35,12 +35,13 @@ import { ReactComponent as SortIcon } from 'images/icons/sort.svg';
|
||||||
import { ReactComponent as TrashIcon } from 'images/icons/trash.svg';
|
import { ReactComponent as TrashIcon } from 'images/icons/trash.svg';
|
||||||
import { ReactComponent as WarningIcon } from 'images/icons/warning.svg';
|
import { ReactComponent as WarningIcon } from 'images/icons/warning.svg';
|
||||||
|
|
||||||
type Icon =
|
type IconName =
|
||||||
| 'cancel-x'
|
| 'cancel-x'
|
||||||
| 'check'
|
| 'check'
|
||||||
| 'checkbox-half'
|
| 'checkbox-half'
|
||||||
| 'checkbox-off'
|
| 'checkbox-off'
|
||||||
| 'checkbox-on'
|
| 'checkbox-on'
|
||||||
|
| 'close'
|
||||||
| 'compass'
|
| 'compass'
|
||||||
| 'dataset-physical'
|
| 'dataset-physical'
|
||||||
| 'dataset-virtual'
|
| 'dataset-virtual'
|
||||||
|
|
@ -53,7 +54,10 @@ type Icon =
|
||||||
| 'trash'
|
| 'trash'
|
||||||
| 'warning';
|
| 'warning';
|
||||||
|
|
||||||
const iconsRegistry: { [key in Icon]: React.ComponentType } = {
|
const iconsRegistry: Record<
|
||||||
|
IconName,
|
||||||
|
React.ComponentType<SVGProps<SVGSVGElement>>
|
||||||
|
> = {
|
||||||
'cancel-x': CancelXIcon,
|
'cancel-x': CancelXIcon,
|
||||||
'checkbox-half': CheckboxHalfIcon,
|
'checkbox-half': CheckboxHalfIcon,
|
||||||
'checkbox-off': CheckboxOffIcon,
|
'checkbox-off': CheckboxOffIcon,
|
||||||
|
|
@ -63,6 +67,7 @@ const iconsRegistry: { [key in Icon]: React.ComponentType } = {
|
||||||
'sort-asc': SortAscIcon,
|
'sort-asc': SortAscIcon,
|
||||||
'sort-desc': SortDescIcon,
|
'sort-desc': SortDescIcon,
|
||||||
check: CheckIcon,
|
check: CheckIcon,
|
||||||
|
close: CloseIcon,
|
||||||
compass: CompassIcon,
|
compass: CompassIcon,
|
||||||
error: ErrorIcon,
|
error: ErrorIcon,
|
||||||
pencil: PencilIcon,
|
pencil: PencilIcon,
|
||||||
|
|
@ -72,14 +77,12 @@ const iconsRegistry: { [key in Icon]: React.ComponentType } = {
|
||||||
warning: WarningIcon,
|
warning: WarningIcon,
|
||||||
};
|
};
|
||||||
interface IconProps extends SVGProps<SVGSVGElement> {
|
interface IconProps extends SVGProps<SVGSVGElement> {
|
||||||
name: Icon;
|
name: IconName;
|
||||||
}
|
}
|
||||||
|
|
||||||
const Icon = ({ name, ...rest }: IconProps) => {
|
const Icon = ({ name, color = '#666666', ...rest }: IconProps) => {
|
||||||
const Component = iconsRegistry[name];
|
const Component = iconsRegistry[name];
|
||||||
return <Component {...rest} />;
|
return <Component color={color} {...rest} />;
|
||||||
};
|
};
|
||||||
|
|
||||||
export default styled(Icon)<{}>`
|
export default Icon;
|
||||||
color: #666666;
|
|
||||||
`;
|
|
||||||
|
|
|
||||||
|
|
@ -336,6 +336,7 @@ class Chart extends React.Component {
|
||||||
timeout={timeout}
|
timeout={timeout}
|
||||||
triggerQuery={chart.triggerQuery}
|
triggerQuery={chart.triggerQuery}
|
||||||
vizType={slice.viz_type}
|
vizType={slice.viz_type}
|
||||||
|
owners={slice.owners}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -134,6 +134,7 @@ export default function (bootstrapData) {
|
||||||
datasource: slice.form_data.datasource,
|
datasource: slice.form_data.datasource,
|
||||||
description: slice.description,
|
description: slice.description,
|
||||||
description_markeddown: slice.description_markeddown,
|
description_markeddown: slice.description_markeddown,
|
||||||
|
owners: slice.owners,
|
||||||
modified: slice.modified,
|
modified: slice.modified,
|
||||||
changed_on: new Date(slice.changed_on).getTime(),
|
changed_on: new Date(slice.changed_on).getTime(),
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -68,6 +68,7 @@ export const slicePropShape = PropTypes.shape({
|
||||||
viz_type: PropTypes.string.isRequired,
|
viz_type: PropTypes.string.isRequired,
|
||||||
description: PropTypes.string,
|
description: PropTypes.string,
|
||||||
description_markeddown: PropTypes.string,
|
description_markeddown: PropTypes.string,
|
||||||
|
owners: PropTypes.arrayOf(PropTypes.string).isRequired,
|
||||||
});
|
});
|
||||||
|
|
||||||
export const filterIndicatorPropShape = PropTypes.shape({
|
export const filterIndicatorPropShape = PropTypes.shape({
|
||||||
|
|
|
||||||
|
|
@ -72,6 +72,7 @@ class ExploreChartPanel extends React.PureComponent {
|
||||||
errorMessage={this.props.errorMessage}
|
errorMessage={this.props.errorMessage}
|
||||||
formData={this.props.form_data}
|
formData={this.props.form_data}
|
||||||
onQuery={this.props.onQuery}
|
onQuery={this.props.onQuery}
|
||||||
|
owners={this.props?.slice?.owners}
|
||||||
queryResponse={chart.queryResponse}
|
queryResponse={chart.queryResponse}
|
||||||
refreshOverlayVisible={this.props.refreshOverlayVisible}
|
refreshOverlayVisible={this.props.refreshOverlayVisible}
|
||||||
setControlValue={this.props.actions.setControlValue}
|
setControlValue={this.props.actions.setControlValue}
|
||||||
|
|
|
||||||
|
|
@ -16,9 +16,22 @@
|
||||||
* specific language governing permissions and limitations
|
* specific language governing permissions and limitations
|
||||||
* under the License.
|
* under the License.
|
||||||
*/
|
*/
|
||||||
|
import getErrorMessageComponentRegistry from 'src/components/ErrorMessage/getErrorMessageComponentRegistry';
|
||||||
|
import { ErrorTypeEnum } from 'src/components/ErrorMessage/types';
|
||||||
|
import TimeoutErrorMessage from 'src/components/ErrorMessage/TimeoutErrorMessage';
|
||||||
|
|
||||||
import setupErrorMessagesExtra from './setupErrorMessagesExtra';
|
import setupErrorMessagesExtra from './setupErrorMessagesExtra';
|
||||||
|
|
||||||
export default function setupErrorMessages() {
|
export default function setupErrorMessages() {
|
||||||
// TODO: Register error messages to the ErrorMessageComponentRegistry once implemented
|
const errorMessageComponentRegistry = getErrorMessageComponentRegistry();
|
||||||
|
|
||||||
|
errorMessageComponentRegistry.registerValue(
|
||||||
|
ErrorTypeEnum.FRONTEND_TIMEOUT_ERROR,
|
||||||
|
TimeoutErrorMessage,
|
||||||
|
);
|
||||||
|
errorMessageComponentRegistry.registerValue(
|
||||||
|
ErrorTypeEnum.BACKEND_TIMEOUT_ERROR,
|
||||||
|
TimeoutErrorMessage,
|
||||||
|
);
|
||||||
setupErrorMessagesExtra();
|
setupErrorMessagesExtra();
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -136,3 +136,5 @@ export function applyFormattingToTabularData(data) {
|
||||||
/* eslint-enable no-underscore-dangle */
|
/* eslint-enable no-underscore-dangle */
|
||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export const noOp = () => undefined;
|
||||||
|
|
|
||||||
|
|
@ -18,7 +18,10 @@
|
||||||
*/
|
*/
|
||||||
import { SupersetClientResponse } from '@superset-ui/connection';
|
import { SupersetClientResponse } from '@superset-ui/connection';
|
||||||
import { t } from '@superset-ui/translation';
|
import { t } from '@superset-ui/translation';
|
||||||
import { SupersetError } from 'src/components/ErrorMessage/types';
|
import {
|
||||||
|
SupersetError,
|
||||||
|
ErrorTypeEnum,
|
||||||
|
} from 'src/components/ErrorMessage/types';
|
||||||
import COMMON_ERR_MESSAGES from './errorMessages';
|
import COMMON_ERR_MESSAGES from './errorMessages';
|
||||||
|
|
||||||
// The response always contains an error attribute, can contain anything from the
|
// The response always contains an error attribute, can contain anything from the
|
||||||
|
|
@ -84,6 +87,38 @@ export default function getClientErrorObject(
|
||||||
resolve({ ...responseObject, error: errorText });
|
resolve({ ...responseObject, error: errorText });
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
} else if (
|
||||||
|
'statusText' in response &&
|
||||||
|
response.statusText === 'timeout'
|
||||||
|
) {
|
||||||
|
resolve({
|
||||||
|
...responseObject,
|
||||||
|
error: 'Request timed out',
|
||||||
|
errors: [
|
||||||
|
{
|
||||||
|
error_type: ErrorTypeEnum.FRONTEND_TIMEOUT_ERROR,
|
||||||
|
extra: {
|
||||||
|
timeout: 1,
|
||||||
|
issue_codes: [
|
||||||
|
{
|
||||||
|
code: 1000,
|
||||||
|
message: t(
|
||||||
|
'Issue 1000 - The datasource is too large to query.',
|
||||||
|
),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
code: 1001,
|
||||||
|
message: t(
|
||||||
|
'Issue 1001 - The database is under an unusual load.',
|
||||||
|
),
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
level: 'error',
|
||||||
|
message: 'Request timed out',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
});
|
||||||
} else {
|
} else {
|
||||||
// fall back to Response.statusText or generic error of we cannot read the response
|
// fall back to Response.statusText or generic error of we cannot read the response
|
||||||
const error =
|
const error =
|
||||||
|
|
|
||||||
|
|
@ -19,6 +19,7 @@ from enum import Enum
|
||||||
from typing import Any, Dict, Optional
|
from typing import Any, Dict, Optional
|
||||||
|
|
||||||
from dataclasses import dataclass
|
from dataclasses import dataclass
|
||||||
|
from flask_babel import gettext as _
|
||||||
|
|
||||||
|
|
||||||
class SupersetErrorType(str, Enum):
|
class SupersetErrorType(str, Enum):
|
||||||
|
|
@ -46,6 +47,23 @@ class SupersetErrorType(str, Enum):
|
||||||
DATASOURCE_SECURITY_ACCESS_ERROR = "DATASOURCE_SECURITY_ACCESS_ERROR"
|
DATASOURCE_SECURITY_ACCESS_ERROR = "DATASOURCE_SECURITY_ACCESS_ERROR"
|
||||||
MISSING_OWNERSHIP_ERROR = "MISSING_OWNERSHIP_ERROR"
|
MISSING_OWNERSHIP_ERROR = "MISSING_OWNERSHIP_ERROR"
|
||||||
|
|
||||||
|
# Other errors
|
||||||
|
BACKEND_TIMEOUT_ERROR = "BACKEND_TIMEOUT_ERROR"
|
||||||
|
|
||||||
|
|
||||||
|
ERROR_TYPES_TO_ISSUE_CODES_MAPPING = {
|
||||||
|
SupersetErrorType.BACKEND_TIMEOUT_ERROR: [
|
||||||
|
{
|
||||||
|
"code": 1000,
|
||||||
|
"message": _("Issue 1000 - The datasource is too large to query."),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"code": 1001,
|
||||||
|
"message": _("Issue 1001 - The database is under an unusual load."),
|
||||||
|
},
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
class ErrorLevel(str, Enum):
|
class ErrorLevel(str, Enum):
|
||||||
"""
|
"""
|
||||||
|
|
@ -69,3 +87,13 @@ class SupersetError:
|
||||||
error_type: SupersetErrorType
|
error_type: SupersetErrorType
|
||||||
level: ErrorLevel
|
level: ErrorLevel
|
||||||
extra: Optional[Dict[str, Any]] = None
|
extra: Optional[Dict[str, Any]] = None
|
||||||
|
|
||||||
|
def __post_init__(self) -> None:
|
||||||
|
"""
|
||||||
|
Mutates the extra params with user facing error codes that map to backend
|
||||||
|
errors.
|
||||||
|
"""
|
||||||
|
issue_codes = ERROR_TYPES_TO_ISSUE_CODES_MAPPING.get(self.error_type)
|
||||||
|
if issue_codes:
|
||||||
|
self.extra = self.extra or {}
|
||||||
|
self.extra.update({"issue_codes": issue_codes})
|
||||||
|
|
|
||||||
|
|
@ -18,7 +18,7 @@ from typing import Any, Dict, Optional
|
||||||
|
|
||||||
from flask_babel import gettext as _
|
from flask_babel import gettext as _
|
||||||
|
|
||||||
from superset.errors import SupersetError
|
from superset.errors import ErrorLevel, SupersetError, SupersetErrorType
|
||||||
|
|
||||||
|
|
||||||
class SupersetException(Exception):
|
class SupersetException(Exception):
|
||||||
|
|
@ -37,7 +37,19 @@ class SupersetException(Exception):
|
||||||
|
|
||||||
|
|
||||||
class SupersetTimeoutException(SupersetException):
|
class SupersetTimeoutException(SupersetException):
|
||||||
pass
|
status = 408
|
||||||
|
|
||||||
|
def __init__(
|
||||||
|
self,
|
||||||
|
error_type: SupersetErrorType,
|
||||||
|
message: str,
|
||||||
|
level: ErrorLevel,
|
||||||
|
extra: Optional[Dict[str, Any]],
|
||||||
|
) -> None:
|
||||||
|
super(SupersetTimeoutException, self).__init__(message)
|
||||||
|
self.error = SupersetError(
|
||||||
|
error_type=error_type, message=message, level=level, extra=extra
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
class SupersetSecurityException(SupersetException):
|
class SupersetSecurityException(SupersetException):
|
||||||
|
|
|
||||||
|
|
@ -177,17 +177,20 @@ class Slice(
|
||||||
data["error"] = str(ex)
|
data["error"] = str(ex)
|
||||||
return {
|
return {
|
||||||
"cache_timeout": self.cache_timeout,
|
"cache_timeout": self.cache_timeout,
|
||||||
|
"changed_on": self.changed_on.isoformat(),
|
||||||
|
"changed_on_humanized": self.changed_on_humanized,
|
||||||
"datasource": self.datasource_name,
|
"datasource": self.datasource_name,
|
||||||
"description": self.description,
|
"description": self.description,
|
||||||
"description_markeddown": self.description_markeddown,
|
"description_markeddown": self.description_markeddown,
|
||||||
"edit_url": self.edit_url,
|
"edit_url": self.edit_url,
|
||||||
"form_data": self.form_data,
|
"form_data": self.form_data,
|
||||||
|
"modified": self.modified(),
|
||||||
|
"owners": [
|
||||||
|
f"{owner.first_name} {owner.last_name}" for owner in self.owners
|
||||||
|
],
|
||||||
"slice_id": self.id,
|
"slice_id": self.id,
|
||||||
"slice_name": self.slice_name,
|
"slice_name": self.slice_name,
|
||||||
"slice_url": self.slice_url,
|
"slice_url": self.slice_url,
|
||||||
"modified": self.modified(),
|
|
||||||
"changed_on_humanized": self.changed_on_humanized,
|
|
||||||
"changed_on": self.changed_on.isoformat(),
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@property
|
@property
|
||||||
|
|
|
||||||
|
|
@ -79,6 +79,7 @@ from sqlalchemy.engine.reflection import Inspector
|
||||||
from sqlalchemy.sql.type_api import Variant
|
from sqlalchemy.sql.type_api import Variant
|
||||||
from sqlalchemy.types import TEXT, TypeDecorator
|
from sqlalchemy.types import TEXT, TypeDecorator
|
||||||
|
|
||||||
|
from superset.errors import ErrorLevel, SupersetErrorType
|
||||||
from superset.exceptions import (
|
from superset.exceptions import (
|
||||||
CertificateException,
|
CertificateException,
|
||||||
SupersetException,
|
SupersetException,
|
||||||
|
|
@ -617,7 +618,12 @@ class timeout: # pylint: disable=invalid-name
|
||||||
self, signum: int, frame: Any
|
self, signum: int, frame: Any
|
||||||
) -> None:
|
) -> None:
|
||||||
logger.error("Process timed out")
|
logger.error("Process timed out")
|
||||||
raise SupersetTimeoutException(self.error_message)
|
raise SupersetTimeoutException(
|
||||||
|
error_type=SupersetErrorType.BACKEND_TIMEOUT_ERROR,
|
||||||
|
message=self.error_message,
|
||||||
|
level=ErrorLevel.ERROR,
|
||||||
|
extra={"timeout": self.seconds},
|
||||||
|
)
|
||||||
|
|
||||||
def __enter__(self) -> None:
|
def __enter__(self) -> None:
|
||||||
try:
|
try:
|
||||||
|
|
|
||||||
|
|
@ -48,7 +48,11 @@ from superset import (
|
||||||
)
|
)
|
||||||
from superset.connectors.sqla import models
|
from superset.connectors.sqla import models
|
||||||
from superset.errors import ErrorLevel, SupersetError, SupersetErrorType
|
from superset.errors import ErrorLevel, SupersetError, SupersetErrorType
|
||||||
from superset.exceptions import SupersetException, SupersetSecurityException
|
from superset.exceptions import (
|
||||||
|
SupersetException,
|
||||||
|
SupersetSecurityException,
|
||||||
|
SupersetTimeoutException,
|
||||||
|
)
|
||||||
from superset.models.helpers import ImportMixin
|
from superset.models.helpers import ImportMixin
|
||||||
from superset.translations.utils import get_language_pack
|
from superset.translations.utils import get_language_pack
|
||||||
from superset.typing import FlaskResponse
|
from superset.typing import FlaskResponse
|
||||||
|
|
@ -176,6 +180,9 @@ def handle_api_exception(
|
||||||
return json_errors_response(
|
return json_errors_response(
|
||||||
errors=[ex.error], status=ex.status, payload=ex.payload
|
errors=[ex.error], status=ex.status, payload=ex.payload
|
||||||
)
|
)
|
||||||
|
except SupersetTimeoutException as ex:
|
||||||
|
logger.warning(ex)
|
||||||
|
return json_errors_response(errors=[ex.error], status=ex.status)
|
||||||
except SupersetException as ex:
|
except SupersetException as ex:
|
||||||
logger.exception(ex)
|
logger.exception(ex)
|
||||||
return json_error_response(
|
return json_error_response(
|
||||||
|
|
|
||||||
|
|
@ -1906,7 +1906,7 @@ class Superset(BaseSupersetView): # pylint: disable=too-many-public-methods
|
||||||
)
|
)
|
||||||
except SupersetTimeoutException as ex:
|
except SupersetTimeoutException as ex:
|
||||||
logger.exception(ex)
|
logger.exception(ex)
|
||||||
return json_error_response(timeout_msg)
|
return json_errors_response([ex.error])
|
||||||
except Exception as ex: # pylint: disable=broad-except
|
except Exception as ex: # pylint: disable=broad-except
|
||||||
return json_error_response(utils.error_msg_from_exception(ex))
|
return json_error_response(utils.error_msg_from_exception(ex))
|
||||||
|
|
||||||
|
|
@ -2151,6 +2151,7 @@ class Superset(BaseSupersetView): # pylint: disable=too-many-public-methods
|
||||||
:param rendered_query: The rendered query (included templates)
|
:param rendered_query: The rendered query (included templates)
|
||||||
:param query: The query SQL (SQLAlchemy) object
|
:param query: The query SQL (SQLAlchemy) object
|
||||||
:return: A Flask Response
|
:return: A Flask Response
|
||||||
|
:raises: SupersetTimeoutException
|
||||||
"""
|
"""
|
||||||
try:
|
try:
|
||||||
timeout = config["SQLLAB_TIMEOUT"]
|
timeout = config["SQLLAB_TIMEOUT"]
|
||||||
|
|
@ -2177,6 +2178,9 @@ class Superset(BaseSupersetView): # pylint: disable=too-many-public-methods
|
||||||
ignore_nan=True,
|
ignore_nan=True,
|
||||||
encoding=None,
|
encoding=None,
|
||||||
)
|
)
|
||||||
|
except SupersetTimeoutException as ex:
|
||||||
|
# re-raise exception for api exception handler
|
||||||
|
raise ex
|
||||||
except Exception as ex: # pylint: disable=broad-except
|
except Exception as ex: # pylint: disable=broad-except
|
||||||
logger.exception("Query %i: %s", query.id, str(ex))
|
logger.exception("Query %i: %s", query.id, str(ex))
|
||||||
return json_error_response(utils.error_msg_from_exception(ex))
|
return json_error_response(utils.error_msg_from_exception(ex))
|
||||||
|
|
@ -2185,6 +2189,7 @@ class Superset(BaseSupersetView): # pylint: disable=too-many-public-methods
|
||||||
return json_success(payload)
|
return json_success(payload)
|
||||||
|
|
||||||
@has_access_api
|
@has_access_api
|
||||||
|
@handle_api_exception
|
||||||
@expose("/sql_json/", methods=["POST"])
|
@expose("/sql_json/", methods=["POST"])
|
||||||
@event_logger.log_this
|
@event_logger.log_this
|
||||||
def sql_json(self) -> FlaskResponse:
|
def sql_json(self) -> FlaskResponse:
|
||||||
|
|
|
||||||
|
|
@ -315,10 +315,13 @@ class TestCore(SupersetTestCase):
|
||||||
def test_slice_data(self):
|
def test_slice_data(self):
|
||||||
# slice data should have some required attributes
|
# slice data should have some required attributes
|
||||||
self.login(username="admin")
|
self.login(username="admin")
|
||||||
slc = self.get_slice("Girls", db.session)
|
slc = self.get_slice(
|
||||||
|
slice_name="Girls", session=db.session, expunge_from_session=False
|
||||||
|
)
|
||||||
slc_data_attributes = slc.data.keys()
|
slc_data_attributes = slc.data.keys()
|
||||||
assert "changed_on" in slc_data_attributes
|
assert "changed_on" in slc_data_attributes
|
||||||
assert "modified" in slc_data_attributes
|
assert "modified" in slc_data_attributes
|
||||||
|
assert "owners" in slc_data_attributes
|
||||||
|
|
||||||
def test_slices(self):
|
def test_slices(self):
|
||||||
# Testing by hitting the two supported end points for all slices
|
# Testing by hitting the two supported end points for all slices
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue