QueryEditorRow: Show query errors next to query in a consistent way across Grafana (#47613)

* Show query errors under each query row

* Testing a more plain error box

* Font size

* Make it green

* Nit UI

* Slight simplification of condition

* New design

* Update

Co-authored-by: Dominik Prokop <dominik.prokop@grafana.com>
This commit is contained in:
Torkel Ödegaard 2022-04-14 12:57:56 +02:00 committed by GitHub
parent c8189e4808
commit 6f31a69bfd
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
11 changed files with 86 additions and 30 deletions

View File

@ -468,7 +468,7 @@ export interface DataQueryError {
error?: string;
};
message?: string;
status?: string;
status?: number;
statusText?: string;
refId?: string;
type?: DataQueryErrorType;

View File

@ -69,6 +69,7 @@ export const getAvailableIcons = () =>
'envelope',
'exchange-alt',
'exclamation-triangle',
'exclamation-circle',
'external-link-alt',
'eye',
'eye-slash',

View File

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

View File

@ -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<ErrorContainerProps> = (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 (
<FadeIn in={showError} duration={duration}>
<Alert severity="error" title={title} className={alertWithTopMargin}>
<Alert severity="error" title={title} topSpacing={2}>
{message}
</Alert>
</FadeIn>

View File

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

View File

@ -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 <ErrorContainer queryError={queryError} />;
}

View File

@ -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(<InspectErrorTab error={error} />);
expect(container.childElementCount).toEqual(1);
expect(screen.getByText('status:')).toBeInTheDocument();
expect(screen.getByText('"400"')).toBeInTheDocument();
expect(screen.getByText('400')).toBeInTheDocument();
});
});

View File

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

View File

@ -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<TQuery extends DataQuery> {
data: PanelData;
@ -382,7 +383,7 @@ export class QueryEditorRow<TQuery extends DataQuery> extends PureComponent<Prop
render() {
const { query, id, index, visualization } = this.props;
const { datasource, showingHelp } = this.state;
const { datasource, showingHelp, data } = this.state;
const isDisabled = query.hide;
const rowClasses = classNames('query-editor-row', {
@ -419,6 +420,7 @@ export class QueryEditorRow<TQuery extends DataQuery> extends PureComponent<Prop
)}
{editor}
</ErrorBoundaryAlert>
{data?.error && data.error.refId === query.refId && <QueryErrorAlert error={data.error} />}
{visualization}
</div>
</QueryOperationRow>
@ -463,16 +465,12 @@ export interface AngularQueryComponentScope<TQuery extends DataQuery> {
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

View File

@ -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 (
<div className={styles.wrapper}>
<div className={styles.icon}>
<Icon name="exclamation-triangle" />
</div>
<div className={styles.message}>{message}</div>
</div>
);
}
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),
}),
});

View File

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