mirror of
https://github.com/grafana/grafana.git
synced 2025-02-25 18:55:37 -06:00
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:
parent
c8189e4808
commit
6f31a69bfd
@ -468,7 +468,7 @@ export interface DataQueryError {
|
|||||||
error?: string;
|
error?: string;
|
||||||
};
|
};
|
||||||
message?: string;
|
message?: string;
|
||||||
status?: string;
|
status?: number;
|
||||||
statusText?: string;
|
statusText?: string;
|
||||||
refId?: string;
|
refId?: string;
|
||||||
type?: DataQueryErrorType;
|
type?: DataQueryErrorType;
|
||||||
|
@ -69,6 +69,7 @@ export const getAvailableIcons = () =>
|
|||||||
'envelope',
|
'envelope',
|
||||||
'exchange-alt',
|
'exchange-alt',
|
||||||
'exclamation-triangle',
|
'exclamation-triangle',
|
||||||
|
'exclamation-circle',
|
||||||
'external-link-alt',
|
'external-link-alt',
|
||||||
'eye',
|
'eye',
|
||||||
'eye-slash',
|
'eye-slash',
|
||||||
|
@ -11,7 +11,7 @@ describe('ErrorContainer', () => {
|
|||||||
error: 'Error data content',
|
error: 'Error data content',
|
||||||
},
|
},
|
||||||
message: 'Error message',
|
message: 'Error message',
|
||||||
status: 'Error status',
|
status: 500,
|
||||||
statusText: 'Error status text',
|
statusText: 'Error status text',
|
||||||
refId: 'A',
|
refId: 'A',
|
||||||
},
|
},
|
||||||
@ -30,7 +30,7 @@ describe('ErrorContainer', () => {
|
|||||||
message: 'Error data message',
|
message: 'Error data message',
|
||||||
error: 'Error data content',
|
error: 'Error data content',
|
||||||
},
|
},
|
||||||
status: 'Error status',
|
status: 500,
|
||||||
statusText: 'Error status text',
|
statusText: 'Error status text',
|
||||||
refId: 'A',
|
refId: 'A',
|
||||||
},
|
},
|
||||||
|
@ -1,8 +1,7 @@
|
|||||||
import React, { FunctionComponent } from 'react';
|
import React, { FunctionComponent } from 'react';
|
||||||
import { DataQueryError } from '@grafana/data';
|
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 { FadeIn } from 'app/core/components/Animations/FadeIn';
|
||||||
import { css } from '@emotion/css';
|
|
||||||
|
|
||||||
export interface ErrorContainerProps {
|
export interface ErrorContainerProps {
|
||||||
queryError?: DataQueryError;
|
queryError?: DataQueryError;
|
||||||
@ -10,18 +9,14 @@ export interface ErrorContainerProps {
|
|||||||
|
|
||||||
export const ErrorContainer: FunctionComponent<ErrorContainerProps> = (props) => {
|
export const ErrorContainer: FunctionComponent<ErrorContainerProps> = (props) => {
|
||||||
const { queryError } = props;
|
const { queryError } = props;
|
||||||
const theme = useTheme2();
|
|
||||||
const showError = queryError ? true : false;
|
const showError = queryError ? true : false;
|
||||||
const duration = showError ? 100 : 10;
|
const duration = showError ? 100 : 10;
|
||||||
const title = queryError ? 'Query error' : 'Unknown error';
|
const title = queryError ? 'Query error' : 'Unknown error';
|
||||||
const message = queryError?.message || queryError?.data?.message || null;
|
const message = queryError?.message || queryError?.data?.message || null;
|
||||||
const alertWithTopMargin = css`
|
|
||||||
margin-top: ${theme.spacing(2)};
|
|
||||||
`;
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<FadeIn in={showError} duration={duration}>
|
<FadeIn in={showError} duration={duration}>
|
||||||
<Alert severity="error" title={title} className={alertWithTopMargin}>
|
<Alert severity="error" title={title} topSpacing={2}>
|
||||||
{message}
|
{message}
|
||||||
</Alert>
|
</Alert>
|
||||||
</FadeIn>
|
</FadeIn>
|
||||||
|
@ -19,15 +19,14 @@ describe('ResponseErrorContainer', () => {
|
|||||||
expect(errorEl).toHaveTextContent(errorMessage);
|
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';
|
const errorMessage = 'test error';
|
||||||
setup({
|
setup({
|
||||||
refId: 'someId',
|
refId: 'someId',
|
||||||
message: errorMessage,
|
message: errorMessage,
|
||||||
});
|
});
|
||||||
const errorEl = screen.getByTestId(selectors.components.Alert.alertV2('error'));
|
const errorEl = screen.queryByTestId(selectors.components.Alert.alertV2('error'));
|
||||||
expect(errorEl).toBeInTheDocument();
|
expect(errorEl).not.toBeInTheDocument();
|
||||||
expect(errorEl).toHaveTextContent(errorMessage);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
it('shows error.data.message if error.message does not exist', async () => {
|
it('shows error.data.message if error.message does not exist', async () => {
|
||||||
|
@ -9,8 +9,12 @@ interface Props {
|
|||||||
}
|
}
|
||||||
export function ResponseErrorContainer(props: Props) {
|
export function ResponseErrorContainer(props: Props) {
|
||||||
const queryResponse = useSelector((state: StoreState) => state.explore[props.exploreId]?.queryResponse);
|
const queryResponse = useSelector((state: StoreState) => state.explore[props.exploreId]?.queryResponse);
|
||||||
|
|
||||||
const queryError = queryResponse?.state === LoadingState.Error ? queryResponse?.error : undefined;
|
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} />;
|
return <ErrorContainer queryError={queryError} />;
|
||||||
}
|
}
|
||||||
|
@ -62,11 +62,11 @@ describe('InspectErrorTab', () => {
|
|||||||
|
|
||||||
it('should return a jsonFormatter object of error if it has no .data and no .message', () => {
|
it('should return a jsonFormatter object of error if it has no .data and no .message', () => {
|
||||||
const error = {
|
const error = {
|
||||||
status: '400',
|
status: 400,
|
||||||
};
|
};
|
||||||
const { container } = render(<InspectErrorTab error={error} />);
|
const { container } = render(<InspectErrorTab error={error} />);
|
||||||
expect(container.childElementCount).toEqual(1);
|
expect(container.childElementCount).toEqual(1);
|
||||||
expect(screen.getByText('status:')).toBeInTheDocument();
|
expect(screen.getByText('status:')).toBeInTheDocument();
|
||||||
expect(screen.getByText('"400"')).toBeInTheDocument();
|
expect(screen.getByText('400')).toBeInTheDocument();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
@ -75,6 +75,21 @@ describe('filterPanelDataToQuery', () => {
|
|||||||
expect(panelDataA?.state).toBe(LoadingState.Done);
|
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', () => {
|
it('should not set the state to done if the frame is loading and has no errors', () => {
|
||||||
const loadingData: PanelData = {
|
const loadingData: PanelData = {
|
||||||
state: LoadingState.Loading,
|
state: LoadingState.Loading,
|
||||||
|
@ -32,6 +32,7 @@ import { PanelModel } from 'app/features/dashboard/state/PanelModel';
|
|||||||
import { DashboardModel } from 'app/features/dashboard/state/DashboardModel';
|
import { DashboardModel } from 'app/features/dashboard/state/DashboardModel';
|
||||||
import { OperationRowHelp } from 'app/core/components/QueryOperationRow/OperationRowHelp';
|
import { OperationRowHelp } from 'app/core/components/QueryOperationRow/OperationRowHelp';
|
||||||
import { RowActionComponents } from './QueryActionComponent';
|
import { RowActionComponents } from './QueryActionComponent';
|
||||||
|
import { QueryErrorAlert } from './QueryErrorAlert';
|
||||||
|
|
||||||
interface Props<TQuery extends DataQuery> {
|
interface Props<TQuery extends DataQuery> {
|
||||||
data: PanelData;
|
data: PanelData;
|
||||||
@ -382,7 +383,7 @@ export class QueryEditorRow<TQuery extends DataQuery> extends PureComponent<Prop
|
|||||||
|
|
||||||
render() {
|
render() {
|
||||||
const { query, id, index, visualization } = this.props;
|
const { query, id, index, visualization } = this.props;
|
||||||
const { datasource, showingHelp } = this.state;
|
const { datasource, showingHelp, data } = this.state;
|
||||||
const isDisabled = query.hide;
|
const isDisabled = query.hide;
|
||||||
|
|
||||||
const rowClasses = classNames('query-editor-row', {
|
const rowClasses = classNames('query-editor-row', {
|
||||||
@ -419,6 +420,7 @@ export class QueryEditorRow<TQuery extends DataQuery> extends PureComponent<Prop
|
|||||||
)}
|
)}
|
||||||
{editor}
|
{editor}
|
||||||
</ErrorBoundaryAlert>
|
</ErrorBoundaryAlert>
|
||||||
|
{data?.error && data.error.refId === query.refId && <QueryErrorAlert error={data.error} />}
|
||||||
{visualization}
|
{visualization}
|
||||||
</div>
|
</div>
|
||||||
</QueryOperationRow>
|
</QueryOperationRow>
|
||||||
@ -463,8 +465,6 @@ export interface AngularQueryComponentScope<TQuery extends DataQuery> {
|
|||||||
export function filterPanelDataToQuery(data: PanelData, refId: string): PanelData | undefined {
|
export function filterPanelDataToQuery(data: PanelData, refId: string): PanelData | undefined {
|
||||||
const series = data.series.filter((series) => series.refId === refId);
|
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 there was an error with no data, pass it to the QueryEditors
|
||||||
if (data.error && !data.series.length) {
|
if (data.error && !data.series.length) {
|
||||||
return {
|
return {
|
||||||
@ -472,8 +472,6 @@ export function filterPanelDataToQuery(data: PanelData, refId: string): PanelDat
|
|||||||
state: LoadingState.Error,
|
state: LoadingState.Error,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
return undefined;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Only say this is an error if the error links to the query
|
// Only say this is an error if the error links to the query
|
||||||
let state = data.state;
|
let state = data.state;
|
||||||
|
41
public/app/features/query/components/QueryErrorAlert.tsx
Normal file
41
public/app/features/query/components/QueryErrorAlert.tsx
Normal 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),
|
||||||
|
}),
|
||||||
|
});
|
@ -735,10 +735,13 @@ export class LokiDatasource
|
|||||||
}
|
}
|
||||||
|
|
||||||
processError(err: FetchError, target: LokiQuery) {
|
processError(err: FetchError, target: LokiQuery) {
|
||||||
let error = cloneDeep(err);
|
let error: DataQueryError = cloneDeep(err);
|
||||||
if (err.data.message.includes('escape') && target.expr.includes('\\')) {
|
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/.`;
|
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;
|
return error;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Loading…
Reference in New Issue
Block a user