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;
|
||||
};
|
||||
message?: string;
|
||||
status?: string;
|
||||
status?: number;
|
||||
statusText?: string;
|
||||
refId?: string;
|
||||
type?: DataQueryErrorType;
|
||||
|
@ -69,6 +69,7 @@ export const getAvailableIcons = () =>
|
||||
'envelope',
|
||||
'exchange-alt',
|
||||
'exclamation-triangle',
|
||||
'exclamation-circle',
|
||||
'external-link-alt',
|
||||
'eye',
|
||||
'eye-slash',
|
||||
|
@ -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',
|
||||
},
|
||||
|
@ -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>
|
||||
|
@ -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 () => {
|
||||
|
@ -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} />;
|
||||
}
|
||||
|
@ -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();
|
||||
});
|
||||
});
|
||||
|
@ -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,
|
||||
|
@ -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
|
||||
|
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) {
|
||||
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;
|
||||
}
|
||||
|
||||
|
Loading…
Reference in New Issue
Block a user