Skip to content

Commit af392c5

Browse files
authored
Alerting: Always show expression warnings and errors (grafana#74839)
1 parent 5d88b8a commit af392c5

File tree

8 files changed

+105
-43
lines changed

8 files changed

+105
-43
lines changed

public/app/features/alerting/unified/components/expressions/Expression.tsx

Lines changed: 5 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ import React, { FC, useCallback, useState } from 'react';
44

55
import { DataFrame, dateTimeFormat, GrafanaTheme2, isTimeSeriesFrames, LoadingState, PanelData } from '@grafana/data';
66
import { Stack } from '@grafana/experimental';
7-
import { AutoSizeInput, Badge, Button, clearButtonStyles, IconButton, useStyles2 } from '@grafana/ui';
7+
import { AutoSizeInput, Button, clearButtonStyles, IconButton, useStyles2 } from '@grafana/ui';
88
import { ClassicConditions } from 'app/features/expressions/components/ClassicConditions';
99
import { Math } from 'app/features/expressions/components/Math';
1010
import { Reduce } from 'app/features/expressions/components/Reduce';
@@ -23,7 +23,7 @@ import { HoverCard } from '../HoverCard';
2323
import { Spacer } from '../Spacer';
2424
import { AlertStateTag } from '../rules/AlertStateTag';
2525

26-
import { AlertConditionIndicator } from './AlertConditionIndicator';
26+
import { ExpressionStatusIndicator } from './ExpressionStatusIndicator';
2727
import { formatLabels, getSeriesLabels, getSeriesName, getSeriesValue, isEmptySeries } from './util';
2828

2929
interface ExpressionProps {
@@ -302,15 +302,11 @@ const Header: FC<HeaderProps> = ({
302302
<div>{getExpressionLabel(queryType)}</div>
303303
</Stack>
304304
<Spacer />
305-
{/* when we have an evaluation error, we show a badge next to "set as alert condition" */}
306-
{!alertCondition && error && (
307-
<Badge color="red" icon="exclamation-circle" text="Error" tooltip={error.message} />
308-
)}
309-
<AlertConditionIndicator
310-
onSetCondition={() => onSetCondition(query.refId)}
311-
enabled={alertCondition}
305+
<ExpressionStatusIndicator
312306
error={error}
313307
warning={warning}
308+
onSetCondition={() => onSetCondition(query.refId)}
309+
isCondition={alertCondition}
314310
/>
315311
<IconButton
316312
name="trash-alt"
Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
import { screen, render } from '@testing-library/react';
2+
import React from 'react';
3+
4+
import { ExpressionStatusIndicator } from './ExpressionStatusIndicator';
5+
6+
describe('ExpressionStatusIndicator', () => {
7+
it('should render two elements when error and not condition', () => {
8+
render(<ExpressionStatusIndicator isCondition={false} warning={new Error('this is a warning')} />);
9+
10+
expect(screen.getByText('Warning')).toBeInTheDocument();
11+
expect(screen.getByRole('button', { name: 'Set as alert condition' })).toBeInTheDocument();
12+
});
13+
14+
it('should render one element when warning and condition', () => {
15+
render(<ExpressionStatusIndicator isCondition warning={new Error('this is a warning')} />);
16+
17+
expect(screen.getByText('Alert condition')).toBeInTheDocument();
18+
expect(screen.queryByRole('button', { name: 'Set as alert condition' })).not.toBeInTheDocument();
19+
});
20+
21+
it('should render two elements when error and not condition', () => {
22+
render(<ExpressionStatusIndicator isCondition={false} error={new Error('this is a error')} />);
23+
24+
expect(screen.getByText('Error')).toBeInTheDocument();
25+
expect(screen.getByRole('button', { name: 'Set as alert condition' })).toBeInTheDocument();
26+
});
27+
28+
it('should render one element when error and condition', () => {
29+
render(<ExpressionStatusIndicator isCondition error={new Error('this is a error')} />);
30+
31+
expect(screen.getByText('Alert condition')).toBeInTheDocument();
32+
expect(screen.queryByRole('button', { name: 'Set as alert condition' })).not.toBeInTheDocument();
33+
});
34+
35+
it('should render one element if condition', () => {
36+
render(<ExpressionStatusIndicator isCondition />);
37+
38+
expect(screen.queryByText('Error')).not.toBeInTheDocument();
39+
expect(screen.queryByText('Warning')).not.toBeInTheDocument();
40+
expect(screen.getByText('Alert condition')).toBeInTheDocument();
41+
});
42+
43+
it('should render one element if not condition', () => {
44+
render(<ExpressionStatusIndicator isCondition={false} />);
45+
46+
expect(screen.queryByText('Error')).not.toBeInTheDocument();
47+
expect(screen.queryByText('Warning')).not.toBeInTheDocument();
48+
expect(screen.queryByRole('button', { name: 'Alert condition' })).not.toBeInTheDocument();
49+
expect(screen.getByRole('button', { name: 'Set as alert condition' })).toBeInTheDocument();
50+
});
51+
});

public/app/features/alerting/unified/components/expressions/AlertConditionIndicator.tsx renamed to public/app/features/alerting/unified/components/expressions/ExpressionStatusIndicator.tsx

Lines changed: 24 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -5,36 +5,47 @@ import { GrafanaTheme2 } from '@grafana/data';
55
import { Badge, clearButtonStyles, useStyles2 } from '@grafana/ui';
66

77
interface AlertConditionProps {
8-
enabled?: boolean;
9-
error?: Error;
108
warning?: Error;
9+
error?: Error;
10+
isCondition?: boolean;
1111
onSetCondition?: () => void;
1212
}
1313

14-
export const AlertConditionIndicator = ({ enabled = false, error, warning, onSetCondition }: AlertConditionProps) => {
14+
export const ExpressionStatusIndicator = ({ error, warning, isCondition, onSetCondition }: AlertConditionProps) => {
1515
const styles = useStyles2(getStyles);
1616

17-
if (enabled && error) {
17+
const elements: JSX.Element[] = [];
18+
19+
if (error && isCondition) {
1820
return <Badge color="red" icon="exclamation-circle" text="Alert condition" tooltip={error.message} />;
21+
} else if (error) {
22+
elements.push(<Badge key="error" color="red" icon="exclamation-circle" text="Error" tooltip={error.message} />);
1923
}
2024

21-
if (enabled && warning) {
25+
if (warning && isCondition) {
2226
return <Badge color="orange" icon="exclamation-triangle" text="Alert condition" tooltip={warning.message} />;
27+
} else if (warning) {
28+
elements.push(
29+
<Badge key="warning" color="orange" icon="exclamation-triangle" text="Warning" tooltip={warning.message} />
30+
);
2331
}
2432

25-
if (enabled && !error && !warning) {
26-
return <Badge color="green" icon="check" text="Alert condition" />;
27-
}
28-
29-
if (!enabled) {
30-
return (
31-
<button type="button" className={styles.actionLink} onClick={() => onSetCondition && onSetCondition()}>
33+
if (isCondition) {
34+
elements.unshift(<Badge key="condition" color="green" icon="check" text="Alert condition" />);
35+
} else {
36+
elements.unshift(
37+
<button
38+
key="make-condition"
39+
type="button"
40+
className={styles.actionLink}
41+
onClick={() => onSetCondition && onSetCondition()}
42+
>
3243
Set as alert condition
3344
</button>
3445
);
3546
}
3647

37-
return null;
48+
return <>{elements}</>;
3849
};
3950

4051
const getStyles = (theme: GrafanaTheme2) => {

public/app/features/alerting/unified/components/rule-editor/ExpressionsEditor.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -46,7 +46,7 @@ export const ExpressionsEditor = ({
4646

4747
const isAlertCondition = condition === query.refId;
4848
const error = data ? errorFromPreviewData(data) : undefined;
49-
const warning = isAlertCondition && data ? warningFromSeries(data.series) : undefined;
49+
const warning = data ? warningFromSeries(data.series) : undefined;
5050

5151
return (
5252
<Expression

public/app/features/alerting/unified/components/rule-editor/QueryRows.tsx

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ import { getDatasourceSrv } from 'app/features/plugins/datasource_srv';
1818
import { AlertDataQuery, AlertQuery } from 'app/types/unified-alerting-dto';
1919

2020
import { AlertQueryOptions, EmptyQueryWrapper, QueryWrapper } from './QueryWrapper';
21-
import { errorFromPreviewData, getThresholdsForQueries } from './util';
21+
import { errorFromCurrentCondition, errorFromPreviewData, getThresholdsForQueries } from './util';
2222

2323
interface Props {
2424
// The query configuration
@@ -156,12 +156,18 @@ export class QueryRows extends PureComponent<Props> {
156156
<div ref={provided.innerRef} {...provided.droppableProps}>
157157
<Stack direction="column">
158158
{queries.map((query, index) => {
159+
const isCondition = this.props.condition === query.refId;
159160
const data: PanelData = this.props.data?.[query.refId] ?? {
160161
series: [],
161162
state: LoadingState.NotStarted,
162163
};
163164
const dsSettings = this.getDataSourceSettings(query);
164-
const error = data ? errorFromPreviewData(data) : undefined;
165+
let error: Error | undefined = undefined;
166+
if (data && isCondition) {
167+
error = errorFromCurrentCondition(data);
168+
} else if (data) {
169+
error = errorFromPreviewData(data);
170+
}
165171

166172
if (!dsSettings) {
167173
return (

public/app/features/alerting/unified/components/rule-editor/QueryWrapper.tsx

Lines changed: 6 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -14,12 +14,12 @@ import {
1414
} from '@grafana/data';
1515
import { Stack } from '@grafana/experimental';
1616
import { DataQuery } from '@grafana/schema';
17-
import { Badge, GraphTresholdsStyleMode, Icon, InlineField, Input, Tooltip, useStyles2 } from '@grafana/ui';
17+
import { GraphTresholdsStyleMode, Icon, InlineField, Input, Tooltip, useStyles2 } from '@grafana/ui';
1818
import { QueryEditorRow } from 'app/features/query/components/QueryEditorRow';
1919
import { AlertQuery } from 'app/types/unified-alerting-dto';
2020

2121
import { msToSingleUnitDuration } from '../../utils/time';
22-
import { AlertConditionIndicator } from '../expressions/AlertConditionIndicator';
22+
import { ExpressionStatusIndicator } from '../expressions/ExpressionStatusIndicator';
2323

2424
import { QueryOptions } from './QueryOptions';
2525
import { VizWrapper } from './VizWrapper';
@@ -122,7 +122,7 @@ export const QueryWrapper = ({
122122
const isAlertCondition = condition === query.refId;
123123

124124
return (
125-
<Stack direction="row" alignItems="baseline" gap={1}>
125+
<Stack direction="row" alignItems="center" gap={1}>
126126
<SelectingDataSourceTooltip />
127127
<QueryOptions
128128
onChangeTimeRange={onChangeTimeRange}
@@ -131,15 +131,11 @@ export const QueryWrapper = ({
131131
onChangeQueryOptions={onChangeQueryOptions}
132132
index={index}
133133
/>
134-
135-
<AlertConditionIndicator
136-
onSetCondition={() => onSetCondition(query.refId)}
137-
enabled={isAlertCondition}
134+
<ExpressionStatusIndicator
138135
error={error}
136+
onSetCondition={() => onSetCondition(query.refId)}
137+
isCondition={isAlertCondition}
139138
/>
140-
{!isAlertCondition && error && (
141-
<Badge color="red" icon="exclamation-circle" text="Error" tooltip={error.message} />
142-
)}
143139
</Stack>
144140
);
145141
}

public/app/features/alerting/unified/components/rule-editor/query-and-alert-condition/QueryAndExpressionsStep.tsx

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@ import { NeedHelpInfo } from '../NeedHelpInfo';
2525
import { QueryEditor } from '../QueryEditor';
2626
import { RecordingRuleEditor } from '../RecordingRuleEditor';
2727
import { RuleEditorSection } from '../RuleEditorSection';
28-
import { errorFromSeries, findRenamedDataQueryReferences, refIdExists } from '../util';
28+
import { errorFromCurrentCondition, errorFromPreviewData, findRenamedDataQueryReferences, refIdExists } from '../util';
2929

3030
import { CloudDataSourceSelector } from './CloudDataSourceSelector';
3131
import { SmartAlertTypeDetector } from './SmartAlertTypeDetector';
@@ -107,11 +107,13 @@ export const QueryAndExpressionsStep = ({ editingExistingRule, onDataChange }: P
107107
useEffect(() => {
108108
const currentCondition = getValues('condition');
109109

110-
if (!currentCondition || RuleFormType.cloudRecording) {
110+
if (!currentCondition || !queryPreviewData[currentCondition]) {
111111
return;
112112
}
113113

114-
const error = errorFromSeries(queryPreviewData[currentCondition]?.series || []);
114+
const error =
115+
errorFromPreviewData(queryPreviewData[currentCondition]) ??
116+
errorFromCurrentCondition(queryPreviewData[currentCondition]);
115117
onDataChange(error?.message || '');
116118
}, [queryPreviewData, getValues, onDataChange]);
117119

public/app/features/alerting/unified/components/rule-editor/util.ts

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -95,12 +95,13 @@ export function checkForPathSeparator(value: string): ValidateResult {
9595
return true;
9696
}
9797

98-
export function errorFromSeries(series: DataFrame[]): Error | undefined {
99-
if (series.length === 0) {
98+
// this function assumes we've already checked if the data passed in to the function is of the alert condition
99+
export function errorFromCurrentCondition(data: PanelData): Error | undefined {
100+
if (data.series.length === 0) {
100101
return;
101102
}
102103

103-
const isTimeSeriesResults = isTimeSeriesFrames(series);
104+
const isTimeSeriesResults = isTimeSeriesFrames(data.series);
104105

105106
let error;
106107
if (isTimeSeriesResults) {
@@ -116,8 +117,7 @@ export function errorFromPreviewData(data: PanelData): Error | undefined {
116117
return new Error(data.errors[0].message);
117118
}
118119

119-
// if none, return errors from series
120-
return errorFromSeries(data.series);
120+
return;
121121
}
122122

123123
export function warningFromSeries(series: DataFrame[]): Error | undefined {

0 commit comments

Comments
 (0)