Alerting: Allow alert rule pausing from API (#62326)

* Add is_paused attr to the POST alert rule group endpoint

* Add is_paused to alerting API POST alert rule group

* Fixed tests

* Add is_paused to alerting gettable endpoints

* Fix integration tests

* Alerting: allow to pause existing rules (#62401)

* Display Pause Rule switch in Editing Rule form

* add isPaused property to form interface and dto

* map isPaused prop with is_paused value from DTO

Also update test snapshots

* Append '(Paused)' text on alert list state column when appropriate

* Change Switch styles according to discussion with UX

Also adding a tooltip with info what this means

* Adjust styles

* Fix alignment and isPaused type definition

Co-authored-by: gillesdemey <gilles.de.mey@gmail.com>

* Fix test

* Fix test

* Fix RuleList test

---------

Co-authored-by: gillesdemey <gilles.de.mey@gmail.com>

* wip

* Fix tests and add comments to clarify AlertRuleWithOptionals

* Fix one more test

* Fix tests

* Fix typo in comment

* Fix alert rule(s) cannot be paused via API

* Add integration tests for alerting api pausing flow

* Remove duplicated integration test

---------

Co-authored-by: Virginia Cepeda <virginia.cepeda@grafana.com>
Co-authored-by: gillesdemey <gilles.de.mey@gmail.com>
Co-authored-by: George Robinson <george.robinson@grafana.com>
This commit is contained in:
Alex Moreno
2023-02-01 13:15:03 +01:00
committed by GitHub
parent c0865c863d
commit 53945afedf
28 changed files with 328 additions and 46 deletions

View File

@@ -210,6 +210,7 @@ describe('RuleEditor grafana managed rules', () => {
condition: 'B',
data: getDefaultQueries(),
exec_err_state: GrafanaAlertStateDecision.Error,
is_paused: false,
no_data_state: 'NoData',
title: 'my great new rule',
},

View File

@@ -160,6 +160,7 @@ describe('RuleEditor grafana managed rules', () => {
condition: 'B',
data: getDefaultQueries(),
exec_err_state: GrafanaAlertStateDecision.Error,
is_paused: false,
no_data_state: 'NoData',
title: 'my great new rule',
},

View File

@@ -371,8 +371,8 @@ describe('RuleList', () => {
const instanceRows = byTestId('row').getAll(instancesTable);
expect(instanceRows).toHaveLength(2);
expect(instanceRows![0]).toHaveTextContent('Firingfoo=barseverity=warning2021-03-18 08:47:05');
expect(instanceRows![1]).toHaveTextContent('Firingfoo=bazseverity=error2021-03-18 08:47:05');
expect(instanceRows![0]).toHaveTextContent('Firing foo=barseverity=warning2021-03-18 08:47:05');
expect(instanceRows![1]).toHaveTextContent('Firing foo=bazseverity=error2021-03-18 08:47:05');
// expand details of an instance
await userEvent.click(ui.ruleCollapseToggle.get(instanceRows![0]));

View File

@@ -255,6 +255,7 @@ export const AlertRuleForm: FC<Props> = ({ existing, prefill }) => {
initialFolder={defaultValues.folder}
evaluateEvery={evaluateEvery}
setEvaluateEvery={setEvaluateEvery}
existing={Boolean(existing)}
/>
) : (
<CloudEvaluationBehavior />

View File

@@ -4,7 +4,7 @@ import { RegisterOptions, useFormContext } from 'react-hook-form';
import { GrafanaTheme2, SelectableValue } from '@grafana/data';
import { Stack } from '@grafana/experimental';
import { Button, Field, InlineLabel, Input, InputControl, useStyles2 } from '@grafana/ui';
import { Button, Field, InlineLabel, Input, InputControl, useStyles2, Switch, Tooltip, Icon } from '@grafana/ui';
import { RulerRuleDTO, RulerRuleGroupDTO, RulerRulesConfigDTO } from 'app/types/unified-alerting-dto';
import { logInfo, LogMessages } from '../../Analytics';
@@ -253,14 +253,20 @@ export function GrafanaEvaluationBehavior({
initialFolder,
evaluateEvery,
setEvaluateEvery,
existing,
}: {
initialFolder: RuleForm | null;
evaluateEvery: string;
setEvaluateEvery: (value: string) => void;
existing: boolean;
}) {
const styles = useStyles2(getStyles);
const [showErrorHandling, setShowErrorHandling] = useState(false);
const { watch, setValue } = useFormContext<RuleFormValues>();
const isPaused = watch('isPaused');
return (
// TODO remove "and alert condition" for recording rules
<RuleEditorSection stepNo={3} title="Alert evaluation behavior">
@@ -271,6 +277,31 @@ export function GrafanaEvaluationBehavior({
evaluateEvery={evaluateEvery}
/>
<ForInput evaluateEvery={evaluateEvery} />
{existing && (
<Field htmlFor="pause-alert-switch">
<InputControl
render={() => (
<Stack gap={1} direction="row" alignItems="center">
<Switch
id="pause-alert"
onChange={(value) => {
setValue('isPaused', value.currentTarget.checked);
}}
value={Boolean(isPaused)}
/>
<label htmlFor="pause-alert" className={styles.switchLabel}>
Pause evaluation
<Tooltip placement="top" content="Turn on to pause evaluation for this alert rule." theme={'info'}>
<Icon tabIndex={0} name="info-circle" size="sm" className={styles.infoIcon} />
</Tooltip>
</label>
</Stack>
)}
name="isPaused"
/>
</Field>
)}
</Stack>
<CollapseToggle
isCollapsed={!showErrorHandling}
@@ -341,6 +372,9 @@ const getStyles = (theme: GrafanaTheme2) => ({
margin-right: ${theme.spacing(1)};
color: ${theme.colors.warning.text};
`,
infoIcon: css`
margin-left: 10px;
`,
warningMessage: css`
color: ${theme.colors.warning.text};
`,
@@ -354,4 +388,9 @@ const getStyles = (theme: GrafanaTheme2) => ({
marginTop: css`
margin-top: ${theme.spacing(1)};
`,
switchLabel: css(`
color: ${theme.colors.text.primary},
cursor: 'pointer',
fontSize: ${theme.typography.bodySmall.fontSize},
`),
});

View File

@@ -8,10 +8,11 @@ import { StateTag } from '../StateTag';
interface Props {
state: PromAlertingRuleState | GrafanaAlertState | GrafanaAlertStateWithReason | AlertState;
size?: 'md' | 'sm';
isPaused?: boolean;
}
export const AlertStateTag: FC<Props> = ({ state, size = 'md' }) => (
export const AlertStateTag: FC<Props> = ({ state, isPaused = false, size = 'md' }) => (
<StateTag state={alertStateToState(state)} size={size}>
{alertStateToReadable(state)}
{alertStateToReadable(state)} {isPaused ? ' (Paused)' : ''}
</StateTag>
);

View File

@@ -14,9 +14,10 @@ interface Props {
rule: CombinedRule;
isDeleting: boolean;
isCreating: boolean;
isPaused?: boolean;
}
export const RuleState: FC<Props> = ({ rule, isDeleting, isCreating }) => {
export const RuleState: FC<Props> = ({ rule, isDeleting, isCreating, isPaused }) => {
const style = useStyles2(getStyle);
const { promRule } = rule;
@@ -68,7 +69,7 @@ export const RuleState: FC<Props> = ({ rule, isDeleting, isCreating }) => {
} else if (promRule && isAlertingRule(promRule)) {
return (
<HorizontalGroup align="flex-start">
<AlertStateTag state={promRule.state} />
<AlertStateTag state={promRule.state} isPaused={isPaused} />
{forTime}
</HorizontalGroup>
);

View File

@@ -114,9 +114,13 @@ function useColumns(showSummaryColumn: boolean, showGroupColumn: boolean) {
const { namespace } = rule;
const { rulesSource } = namespace;
const { promRule, rulerRule } = rule;
const isDeleting = !!(hasRuler(rulesSource) && rulerRulesLoaded(rulesSource) && promRule && !rulerRule);
const isCreating = !!(hasRuler(rulesSource) && rulerRulesLoaded(rulesSource) && rulerRule && !promRule);
return <RuleState rule={rule} isDeleting={isDeleting} isCreating={isCreating} />;
const isGrafanaManagedRule = isGrafanaRulerRule(rulerRule);
const isPaused = isGrafanaManagedRule && Boolean(rulerRule.grafana_alert.is_paused);
return <RuleState rule={rule} isDeleting={isDeleting} isCreating={isCreating} isPaused={isPaused} />;
},
size: '165px',
},

View File

@@ -29,6 +29,7 @@ export interface RuleFormValues {
folder: RuleForm | null;
evaluateEvery: string;
evaluateFor: string;
isPaused?: boolean;
// cortex / loki rules
namespace: string;

View File

@@ -12,6 +12,7 @@ exports[`formValuesToRulerGrafanaRuleDTO should correctly convert rule form valu
"condition": "A",
"data": [],
"exec_err_state": "Error",
"is_paused": false,
"no_data_state": "NoData",
"title": "",
},
@@ -49,6 +50,7 @@ exports[`formValuesToRulerGrafanaRuleDTO should not save both instant and range
},
],
"exec_err_state": "Error",
"is_paused": false,
"no_data_state": "NoData",
"title": "",
},

View File

@@ -96,7 +96,7 @@ function listifyLabelsOrAnnotations(item: Labels | Annotations | undefined): Arr
}
export function formValuesToRulerGrafanaRuleDTO(values: RuleFormValues): PostableRuleGrafanaRuleDTO {
const { name, condition, noDataState, execErrState, evaluateFor, queries } = values;
const { name, condition, noDataState, execErrState, evaluateFor, queries, isPaused } = values;
if (condition) {
return {
grafana_alert: {
@@ -105,6 +105,7 @@ export function formValuesToRulerGrafanaRuleDTO(values: RuleFormValues): Postabl
no_data_state: noDataState,
exec_err_state: execErrState,
data: queries.map(fixBothInstantAndRangeQuery),
is_paused: Boolean(isPaused),
},
for: evaluateFor,
annotations: arrayToRecord(values.annotations || []),
@@ -135,6 +136,7 @@ export function rulerRuleToFormValues(ruleWithLocation: RuleWithLocation): RuleF
annotations: listifyLabelsOrAnnotations(rule.annotations),
labels: listifyLabelsOrAnnotations(rule.labels),
folder: { title: namespace, id: ga.namespace_id },
isPaused: ga.is_paused,
};
} else {
throw new Error('Unexpected type of rule for grafana rules source');

View File

@@ -200,6 +200,7 @@ export interface PostableGrafanaRuleDefinition {
no_data_state: GrafanaAlertStateDecision;
exec_err_state: GrafanaAlertStateDecision;
data: AlertQuery[];
is_paused?: boolean;
}
export interface GrafanaRuleDefinition extends PostableGrafanaRuleDefinition {
id?: string;