mirror of
https://github.com/grafana/grafana.git
synced 2025-02-25 18:55:37 -06:00
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:
@@ -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',
|
||||
},
|
||||
|
||||
@@ -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',
|
||||
},
|
||||
|
||||
@@ -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]));
|
||||
|
||||
@@ -255,6 +255,7 @@ export const AlertRuleForm: FC<Props> = ({ existing, prefill }) => {
|
||||
initialFolder={defaultValues.folder}
|
||||
evaluateEvery={evaluateEvery}
|
||||
setEvaluateEvery={setEvaluateEvery}
|
||||
existing={Boolean(existing)}
|
||||
/>
|
||||
) : (
|
||||
<CloudEvaluationBehavior />
|
||||
|
||||
@@ -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},
|
||||
`),
|
||||
});
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
|
||||
@@ -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',
|
||||
},
|
||||
|
||||
@@ -29,6 +29,7 @@ export interface RuleFormValues {
|
||||
folder: RuleForm | null;
|
||||
evaluateEvery: string;
|
||||
evaluateFor: string;
|
||||
isPaused?: boolean;
|
||||
|
||||
// cortex / loki rules
|
||||
namespace: string;
|
||||
|
||||
@@ -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": "",
|
||||
},
|
||||
|
||||
@@ -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');
|
||||
|
||||
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user