diff --git a/packages/grafana-data/src/types/datasource.ts b/packages/grafana-data/src/types/datasource.ts index 69717854e2b..1396140c41e 100644 --- a/packages/grafana-data/src/types/datasource.ts +++ b/packages/grafana-data/src/types/datasource.ts @@ -468,7 +468,7 @@ export interface DataQueryError { error?: string; }; message?: string; - status?: string; + status?: number; statusText?: string; refId?: string; type?: DataQueryErrorType; diff --git a/packages/grafana-ui/src/types/icon.ts b/packages/grafana-ui/src/types/icon.ts index de315cb978e..870cf8b2d82 100644 --- a/packages/grafana-ui/src/types/icon.ts +++ b/packages/grafana-ui/src/types/icon.ts @@ -69,6 +69,7 @@ export const getAvailableIcons = () => 'envelope', 'exchange-alt', 'exclamation-triangle', + 'exclamation-circle', 'external-link-alt', 'eye', 'eye-slash', diff --git a/public/app/features/explore/ErrorContainer.test.tsx b/public/app/features/explore/ErrorContainer.test.tsx index 589cdba349c..accc19c1877 100644 --- a/public/app/features/explore/ErrorContainer.test.tsx +++ b/public/app/features/explore/ErrorContainer.test.tsx @@ -11,7 +11,7 @@ describe('ErrorContainer', () => { error: 'Error data content', }, message: 'Error message', - status: 'Error status', + status: 500, statusText: 'Error status text', refId: 'A', }, @@ -30,7 +30,7 @@ describe('ErrorContainer', () => { message: 'Error data message', error: 'Error data content', }, - status: 'Error status', + status: 500, statusText: 'Error status text', refId: 'A', }, diff --git a/public/app/features/explore/ErrorContainer.tsx b/public/app/features/explore/ErrorContainer.tsx index 675b2dcf6f7..fffa9c10313 100644 --- a/public/app/features/explore/ErrorContainer.tsx +++ b/public/app/features/explore/ErrorContainer.tsx @@ -1,8 +1,7 @@ import React, { FunctionComponent } from 'react'; import { DataQueryError } from '@grafana/data'; -import { Alert, useTheme2 } from '@grafana/ui'; +import { Alert } from '@grafana/ui'; import { FadeIn } from 'app/core/components/Animations/FadeIn'; -import { css } from '@emotion/css'; export interface ErrorContainerProps { queryError?: DataQueryError; @@ -10,18 +9,14 @@ export interface ErrorContainerProps { export const ErrorContainer: FunctionComponent = (props) => { const { queryError } = props; - const theme = useTheme2(); const showError = queryError ? true : false; const duration = showError ? 100 : 10; const title = queryError ? 'Query error' : 'Unknown error'; const message = queryError?.message || queryError?.data?.message || null; - const alertWithTopMargin = css` - margin-top: ${theme.spacing(2)}; - `; return ( - + {message} diff --git a/public/app/features/explore/ResponseErrorContainer.test.tsx b/public/app/features/explore/ResponseErrorContainer.test.tsx index 75982c68a2f..387dc9ee937 100644 --- a/public/app/features/explore/ResponseErrorContainer.test.tsx +++ b/public/app/features/explore/ResponseErrorContainer.test.tsx @@ -19,15 +19,14 @@ describe('ResponseErrorContainer', () => { expect(errorEl).toHaveTextContent(errorMessage); }); - it('shows error if there is refID', async () => { + it('do not show error if there is a refId', async () => { const errorMessage = 'test error'; setup({ refId: 'someId', message: errorMessage, }); - const errorEl = screen.getByTestId(selectors.components.Alert.alertV2('error')); - expect(errorEl).toBeInTheDocument(); - expect(errorEl).toHaveTextContent(errorMessage); + const errorEl = screen.queryByTestId(selectors.components.Alert.alertV2('error')); + expect(errorEl).not.toBeInTheDocument(); }); it('shows error.data.message if error.message does not exist', async () => { diff --git a/public/app/features/explore/ResponseErrorContainer.tsx b/public/app/features/explore/ResponseErrorContainer.tsx index a23ad7fa2f4..5dd5f59f154 100644 --- a/public/app/features/explore/ResponseErrorContainer.tsx +++ b/public/app/features/explore/ResponseErrorContainer.tsx @@ -9,8 +9,12 @@ interface Props { } export function ResponseErrorContainer(props: Props) { const queryResponse = useSelector((state: StoreState) => state.explore[props.exploreId]?.queryResponse); - const queryError = queryResponse?.state === LoadingState.Error ? queryResponse?.error : undefined; + // Errors with ref ids are shown below the corresponding query + if (queryError?.refId) { + return null; + } + return ; } diff --git a/public/app/features/inspector/InspectErrorTab.test.tsx b/public/app/features/inspector/InspectErrorTab.test.tsx index b04e1283d35..07531dbb3e3 100644 --- a/public/app/features/inspector/InspectErrorTab.test.tsx +++ b/public/app/features/inspector/InspectErrorTab.test.tsx @@ -62,11 +62,11 @@ describe('InspectErrorTab', () => { it('should return a jsonFormatter object of error if it has no .data and no .message', () => { const error = { - status: '400', + status: 400, }; const { container } = render(); expect(container.childElementCount).toEqual(1); expect(screen.getByText('status:')).toBeInTheDocument(); - expect(screen.getByText('"400"')).toBeInTheDocument(); + expect(screen.getByText('400')).toBeInTheDocument(); }); }); diff --git a/public/app/features/query/components/QueryEditorRow.test.ts b/public/app/features/query/components/QueryEditorRow.test.ts index 6117923d66c..34953929ce2 100644 --- a/public/app/features/query/components/QueryEditorRow.test.ts +++ b/public/app/features/query/components/QueryEditorRow.test.ts @@ -75,6 +75,21 @@ describe('filterPanelDataToQuery', () => { expect(panelDataA?.state).toBe(LoadingState.Done); }); + it('should return error for query that returns no data, but another query does return data', () => { + const withError = { + ...data, + state: LoadingState.Error, + error: { + message: 'Sad', + refId: 'Q', + }, + }; + + const panelDataB = filterPanelDataToQuery(withError, 'Q'); + expect(panelDataB?.series.length).toBe(0); + expect(panelDataB?.error?.refId).toBe('Q'); + }); + it('should not set the state to done if the frame is loading and has no errors', () => { const loadingData: PanelData = { state: LoadingState.Loading, diff --git a/public/app/features/query/components/QueryEditorRow.tsx b/public/app/features/query/components/QueryEditorRow.tsx index fb7a32444c3..d7a6f156d2e 100644 --- a/public/app/features/query/components/QueryEditorRow.tsx +++ b/public/app/features/query/components/QueryEditorRow.tsx @@ -32,6 +32,7 @@ import { PanelModel } from 'app/features/dashboard/state/PanelModel'; import { DashboardModel } from 'app/features/dashboard/state/DashboardModel'; import { OperationRowHelp } from 'app/core/components/QueryOperationRow/OperationRowHelp'; import { RowActionComponents } from './QueryActionComponent'; +import { QueryErrorAlert } from './QueryErrorAlert'; interface Props { data: PanelData; @@ -382,7 +383,7 @@ export class QueryEditorRow extends PureComponent extends PureComponent + {data?.error && data.error.refId === query.refId && } {visualization} @@ -463,16 +465,12 @@ export interface AngularQueryComponentScope { export function filterPanelDataToQuery(data: PanelData, refId: string): PanelData | undefined { const series = data.series.filter((series) => series.refId === refId); - // No matching series - if (!series.length) { - // If there was an error with no data, pass it to the QueryEditors - if (data.error && !data.series.length) { - return { - ...data, - state: LoadingState.Error, - }; - } - return undefined; + // If there was an error with no data, pass it to the QueryEditors + if (data.error && !data.series.length) { + return { + ...data, + state: LoadingState.Error, + }; } // Only say this is an error if the error links to the query diff --git a/public/app/features/query/components/QueryErrorAlert.tsx b/public/app/features/query/components/QueryErrorAlert.tsx new file mode 100644 index 00000000000..c7cfd73909b --- /dev/null +++ b/public/app/features/query/components/QueryErrorAlert.tsx @@ -0,0 +1,41 @@ +import React from 'react'; +import { DataQueryError, GrafanaTheme2 } from '@grafana/data'; +import { Icon, useStyles2 } from '@grafana/ui'; +import { css } from '@emotion/css'; + +export interface Props { + error: DataQueryError; +} + +export function QueryErrorAlert({ error }: Props) { + const styles = useStyles2(getStyles); + + const message = error?.message ?? error?.data?.message ?? 'Query error'; + + return ( +
+
+ +
+
{message}
+
+ ); +} + +const getStyles = (theme: GrafanaTheme2) => ({ + wrapper: css({ + marginTop: theme.spacing(0.5), + background: theme.colors.background.secondary, + display: 'flex', + }), + icon: css({ + background: theme.colors.error.main, + color: theme.colors.error.contrastText, + padding: theme.spacing(1), + }), + message: css({ + fontSize: theme.typography.bodySmall.fontSize, + fontFamily: theme.typography.fontFamilyMonospace, + padding: theme.spacing(1), + }), +}); diff --git a/public/app/plugins/datasource/loki/datasource.ts b/public/app/plugins/datasource/loki/datasource.ts index 41a71d3709f..9de4a813dbc 100644 --- a/public/app/plugins/datasource/loki/datasource.ts +++ b/public/app/plugins/datasource/loki/datasource.ts @@ -735,10 +735,13 @@ export class LokiDatasource } processError(err: FetchError, target: LokiQuery) { - let error = cloneDeep(err); - if (err.data.message.includes('escape') && target.expr.includes('\\')) { + let error: DataQueryError = cloneDeep(err); + error.refId = target.refId; + + if (error.data && err.data.message.includes('escape') && target.expr.includes('\\')) { error.data.message = `Error: ${err.data.message}. Make sure that all special characters are escaped with \\. For more information on escaping of special characters visit LogQL documentation at https://grafana.com/docs/loki/latest/logql/.`; } + return error; }