Alerting: Fix silence / delete on new list view (#107885)
This commit is contained in:
@@ -1,15 +1,14 @@
|
||||
import { Menu } from '@grafana/ui';
|
||||
import { useAppNotification } from 'app/core/copy/appNotification';
|
||||
import { GrafanaRuleGroupIdentifier } from 'app/types/unified-alerting';
|
||||
import { RulerGrafanaRuleDTO } from 'app/types/unified-alerting-dto';
|
||||
|
||||
import { usePauseRuleInGroup } from '../hooks/ruleGroup/usePauseAlertRule';
|
||||
import { isLoading } from '../hooks/useAsync';
|
||||
import { stringifyErrorLike } from '../utils/misc';
|
||||
import { isPausedRule } from '../utils/rules';
|
||||
|
||||
interface Props {
|
||||
rule: RulerGrafanaRuleDTO;
|
||||
uid: string;
|
||||
isPaused: boolean;
|
||||
groupIdentifier: GrafanaRuleGroupIdentifier;
|
||||
/**
|
||||
* Method invoked after the request to change the paused state has completed
|
||||
@@ -21,20 +20,18 @@ interface Props {
|
||||
* Menu item to display correct text for pausing/resuming an alert,
|
||||
* and triggering API call to do so
|
||||
*/
|
||||
const MenuItemPauseRule = ({ rule, groupIdentifier, onPauseChange }: Props) => {
|
||||
const MenuItemPauseRule = ({ uid, isPaused, groupIdentifier, onPauseChange }: Props) => {
|
||||
const notifyApp = useAppNotification();
|
||||
const [pauseRule, updateState] = usePauseRuleInGroup();
|
||||
|
||||
const [icon, title] = isPausedRule(rule)
|
||||
? ['play' as const, 'Resume evaluation']
|
||||
: ['pause' as const, 'Pause evaluation'];
|
||||
const [icon, title] = isPaused ? ['play' as const, 'Resume evaluation'] : ['pause' as const, 'Pause evaluation'];
|
||||
|
||||
/**
|
||||
* Triggers API call to update the current rule to the new `is_paused` state
|
||||
*/
|
||||
const setRulePause = async (newIsPaused: boolean) => {
|
||||
try {
|
||||
await pauseRule.execute(groupIdentifier, rule.grafana_alert.uid, newIsPaused);
|
||||
await pauseRule.execute(groupIdentifier, uid, newIsPaused);
|
||||
} catch (error) {
|
||||
notifyApp.error(`Failed to ${newIsPaused ? 'pause' : 'resume'} the rule: ${stringifyErrorLike(error)}`);
|
||||
return;
|
||||
@@ -49,7 +46,7 @@ const MenuItemPauseRule = ({ rule, groupIdentifier, onPauseChange }: Props) => {
|
||||
icon={icon}
|
||||
disabled={isLoading(updateState)}
|
||||
onClick={() => {
|
||||
setRulePause(!rule.grafana_alert.is_paused);
|
||||
setRulePause(!isPaused);
|
||||
}}
|
||||
/>
|
||||
);
|
||||
|
||||
@@ -7,7 +7,7 @@ import appEvents from 'app/core/app_events';
|
||||
import MenuItemPauseRule from 'app/features/alerting/unified/components/MenuItemPauseRule';
|
||||
import MoreButton from 'app/features/alerting/unified/components/MoreButton';
|
||||
import { useRulePluginLinkExtension } from 'app/features/alerting/unified/plugins/useRulePluginLinkExtensions';
|
||||
import { Rule, RuleGroupIdentifierV2, RuleIdentifier } from 'app/types/unified-alerting';
|
||||
import { EditableRuleIdentifier, Rule, RuleGroupIdentifierV2, RuleIdentifier } from 'app/types/unified-alerting';
|
||||
import { PromAlertingRuleState, RulerRuleDTO } from 'app/types/unified-alerting-dto';
|
||||
|
||||
import {
|
||||
@@ -18,7 +18,13 @@ import {
|
||||
} from '../../hooks/useAbilities';
|
||||
import { createShareLink, isLocalDevEnv, isOpenSourceEdition } from '../../utils/misc';
|
||||
import * as ruleId from '../../utils/rule-id';
|
||||
import { prometheusRuleType, rulerRuleType } from '../../utils/rules';
|
||||
import {
|
||||
getRuleUID,
|
||||
isEditableRuleIdentifier,
|
||||
isPausedRule,
|
||||
prometheusRuleType,
|
||||
rulerRuleType,
|
||||
} from '../../utils/rules';
|
||||
import { createRelativeUrl } from '../../utils/url';
|
||||
import { DeclareIncidentMenuItem } from '../bridges/DeclareIncidentButton';
|
||||
|
||||
@@ -28,7 +34,7 @@ interface Props {
|
||||
identifier: RuleIdentifier;
|
||||
groupIdentifier: RuleGroupIdentifierV2;
|
||||
handleSilence: () => void;
|
||||
handleDelete: (rule: RulerRuleDTO, groupIdentifier: RuleGroupIdentifierV2) => void;
|
||||
handleDelete: (identifier: EditableRuleIdentifier, groupIdentifier: RuleGroupIdentifierV2) => void;
|
||||
handleDuplicateRule: (identifier: RuleIdentifier) => void;
|
||||
onPauseChange?: () => void;
|
||||
buttonSize?: ComponentSize;
|
||||
@@ -118,10 +124,22 @@ const AlertRuleMenu = ({
|
||||
const showDivider =
|
||||
[canPause, canSilence, shouldShowDeclareIncidentButton, canDuplicate].some(Boolean) && [canExport].some(Boolean);
|
||||
|
||||
// grab the UID from either rulerRule or promRule
|
||||
const ruleUid = getRuleUID(rulerRule ?? promRule);
|
||||
|
||||
const isPaused =
|
||||
(rulerRuleType.grafana.rule(rulerRule) && isPausedRule(rulerRule)) ||
|
||||
(prometheusRuleType.grafana.rule(promRule) && promRule.isPaused);
|
||||
|
||||
const menuItems = (
|
||||
<>
|
||||
{canPause && rulerRuleType.grafana.rule(rulerRule) && groupIdentifier.groupOrigin === 'grafana' && (
|
||||
<MenuItemPauseRule rule={rulerRule} groupIdentifier={groupIdentifier} onPauseChange={onPauseChange} />
|
||||
{canPause && ruleUid && groupIdentifier.groupOrigin === 'grafana' && (
|
||||
<MenuItemPauseRule
|
||||
uid={ruleUid}
|
||||
isPaused={isPaused}
|
||||
groupIdentifier={groupIdentifier}
|
||||
onPauseChange={onPauseChange}
|
||||
/>
|
||||
)}
|
||||
{canSilence && (
|
||||
<Menu.Item
|
||||
@@ -162,14 +180,19 @@ const AlertRuleMenu = ({
|
||||
))}
|
||||
</>
|
||||
)}
|
||||
{canDelete && rulerRule && (
|
||||
{canDelete && (
|
||||
<>
|
||||
<Menu.Divider />
|
||||
<Menu.Item
|
||||
label={t('alerting.common.delete', 'Delete')}
|
||||
icon="trash-alt"
|
||||
destructive
|
||||
onClick={() => handleDelete(rulerRule, groupIdentifier)}
|
||||
onClick={() => {
|
||||
// if the identifier is not for a editable rule I wonder how you even got here.
|
||||
if (isEditableRuleIdentifier(identifier)) {
|
||||
handleDelete(identifier, groupIdentifier);
|
||||
}
|
||||
}}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { isString } from 'lodash';
|
||||
import { useState } from 'react';
|
||||
|
||||
import { Trans, t } from '@grafana/i18n';
|
||||
@@ -16,7 +17,7 @@ import { GRAFANA_RULES_SOURCE_NAME, getRulesSourceName } from '../../utils/datas
|
||||
import { groupIdentifier } from '../../utils/groupIdentifier';
|
||||
import { createViewLink } from '../../utils/misc';
|
||||
import * as ruleId from '../../utils/rule-id';
|
||||
import { rulerRuleType } from '../../utils/rules';
|
||||
import { getRuleUID, prometheusRuleType, rulerRuleType } from '../../utils/rules';
|
||||
import { createRelativeUrl } from '../../utils/url';
|
||||
|
||||
import { RedirectToCloneRule } from './CloneRule';
|
||||
@@ -104,6 +105,12 @@ export const RuleActionsButtons = ({ compact, showViewButton, rule, rulesSource
|
||||
return null;
|
||||
}
|
||||
|
||||
// determine if this rule can be silenced by checking for Grafana Alert rule type and extracting the UID
|
||||
const ruleUid = getRuleUID(rule.rulerRule ?? rule.promRule);
|
||||
const silenceableRule =
|
||||
isString(ruleUid) &&
|
||||
(rulerRuleType.grafana.alertingRule(rule.rulerRule) || prometheusRuleType.grafana.alertingRule(rule.promRule));
|
||||
|
||||
return (
|
||||
<Stack gap={1} alignItems="center" wrap="nowrap">
|
||||
{buttons}
|
||||
@@ -132,8 +139,8 @@ export const RuleActionsButtons = ({ compact, showViewButton, rule, rulesSource
|
||||
buttonSize={buttonSize}
|
||||
/>
|
||||
{deleteModal}
|
||||
{rulerRuleType.grafana.alertingRule(rule.rulerRule) && showSilenceDrawer && (
|
||||
<SilenceGrafanaRuleDrawer rulerRule={rule.rulerRule} onClose={() => setShowSilenceDrawer(false)} />
|
||||
{silenceableRule && showSilenceDrawer && (
|
||||
<SilenceGrafanaRuleDrawer ruleUid={ruleUid} onClose={() => setShowSilenceDrawer(false)} />
|
||||
)}
|
||||
{redirectToClone?.identifier && (
|
||||
<RedirectToCloneRule
|
||||
|
||||
+21
-28
@@ -4,12 +4,11 @@ import { t } from '@grafana/i18n';
|
||||
import { Drawer, Stack } from '@grafana/ui';
|
||||
import { SilencesEditor } from 'app/features/alerting/unified/components/silences/SilencesEditor';
|
||||
import { GRAFANA_RULES_SOURCE_NAME } from 'app/features/alerting/unified/utils/datasource';
|
||||
import { RulerGrafanaRuleDTO } from 'app/types/unified-alerting-dto';
|
||||
|
||||
import { AlertmanagerProvider } from '../../state/AlertmanagerContext';
|
||||
|
||||
type Props = {
|
||||
rulerRule: RulerGrafanaRuleDTO;
|
||||
ruleUid: string;
|
||||
onClose: () => void;
|
||||
};
|
||||
|
||||
@@ -17,32 +16,26 @@ type Props = {
|
||||
* For a given Grafana managed rule, renders a drawer containing silences editor and Alertmanager selection
|
||||
*/
|
||||
const SilenceGrafanaRuleDrawer = React.memo(
|
||||
({ rulerRule, onClose }: Props) => {
|
||||
const { uid } = rulerRule.grafana_alert;
|
||||
|
||||
return (
|
||||
<Drawer
|
||||
title={t('alerting.silence-grafana-rule-drawer.title-silence-alert-rule', 'Silence alert rule')}
|
||||
subtitle="Configure silences to stop notifications from a particular alert rule."
|
||||
onClose={onClose}
|
||||
size="md"
|
||||
>
|
||||
<Stack direction={'column'}>
|
||||
<AlertmanagerProvider accessType="instance">
|
||||
<SilencesEditor
|
||||
ruleUid={uid}
|
||||
alertManagerSourceName={GRAFANA_RULES_SOURCE_NAME}
|
||||
onSilenceCreated={onClose}
|
||||
onCancel={onClose}
|
||||
/>
|
||||
</AlertmanagerProvider>
|
||||
</Stack>
|
||||
</Drawer>
|
||||
);
|
||||
},
|
||||
(prevProps, nextProps) => {
|
||||
return prevProps.rulerRule.grafana_alert.uid === nextProps.rulerRule.grafana_alert.uid;
|
||||
}
|
||||
({ ruleUid, onClose }: Props) => (
|
||||
<Drawer
|
||||
title={t('alerting.silence-grafana-rule-drawer.title-silence-alert-rule', 'Silence alert rule')}
|
||||
subtitle="Configure silences to stop notifications from a particular alert rule."
|
||||
onClose={onClose}
|
||||
size="md"
|
||||
>
|
||||
<Stack direction={'column'}>
|
||||
<AlertmanagerProvider accessType="instance">
|
||||
<SilencesEditor
|
||||
ruleUid={ruleUid}
|
||||
alertManagerSourceName={GRAFANA_RULES_SOURCE_NAME}
|
||||
onSilenceCreated={onClose}
|
||||
onCancel={onClose}
|
||||
/>
|
||||
</AlertmanagerProvider>
|
||||
</Stack>
|
||||
</Drawer>
|
||||
),
|
||||
(prevProps, nextProps) => prevProps.ruleUid === nextProps.ruleUid
|
||||
);
|
||||
|
||||
export default SilenceGrafanaRuleDrawer;
|
||||
|
||||
@@ -40,6 +40,7 @@ const ui = {
|
||||
copyLink: () => byRole('menuitem', { name: /copy link/i }),
|
||||
export: () => byRole('menuitem', { name: /export/i }),
|
||||
delete: () => byRole('menuitem', { name: /delete/i }),
|
||||
pause: () => byRole('menuitem', { name: /pause/i }),
|
||||
},
|
||||
};
|
||||
|
||||
@@ -188,7 +189,7 @@ describe('GrafanaGroupLoader', () => {
|
||||
const menu = byRole('menu').get();
|
||||
expect(menu).toBeInTheDocument();
|
||||
|
||||
// With proper permissions, all 4 menu actions should be available:
|
||||
// With proper permissions, all 6 menu actions should be available:
|
||||
|
||||
// 1. Silence notifications - available for alerting rules (AlertingSilenceCreate permission)
|
||||
expect(ui.menuItems.silence().get()).toBeInTheDocument();
|
||||
@@ -202,9 +203,15 @@ describe('GrafanaGroupLoader', () => {
|
||||
// 4. Export - should be available for Grafana alerting rules (AlertingRuleRead permission)
|
||||
expect(ui.menuItems.export().get()).toBeInTheDocument();
|
||||
|
||||
// Verify that the menu contains all 4 expected menu items
|
||||
// 5. Delete - should be available for Grafana alerting rules (AlertingRuleDelete permission)
|
||||
expect(ui.menuItems.delete().get()).toBeInTheDocument();
|
||||
|
||||
// 6. Pause - should be available for Grafana alerting rules (AlertingRuleUpdate permission)
|
||||
expect(ui.menuItems.pause().get()).toBeInTheDocument();
|
||||
|
||||
// Verify that the menu contains all 6 expected menu items
|
||||
const menuItems = byRole('menuitem').getAll();
|
||||
expect(menuItems.length).toBe(4);
|
||||
expect(menuItems.length).toBe(6);
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
+17
-4
@@ -1,3 +1,4 @@
|
||||
import { isString } from 'lodash';
|
||||
import { useState } from 'react';
|
||||
import { RequireAtLeastOne } from 'type-fest';
|
||||
|
||||
@@ -19,7 +20,13 @@ import { RulerRuleDTO } from 'app/types/unified-alerting-dto';
|
||||
import { logWarning } from '../../Analytics';
|
||||
import { AlertRuleAction, skipToken, useGrafanaPromRuleAbility, useRulerRuleAbility } from '../../hooks/useAbilities';
|
||||
import * as ruleId from '../../utils/rule-id';
|
||||
import { isProvisionedPromRule, isProvisionedRule, prometheusRuleType, rulerRuleType } from '../../utils/rules';
|
||||
import {
|
||||
getRuleUID,
|
||||
isProvisionedPromRule,
|
||||
isProvisionedRule,
|
||||
prometheusRuleType,
|
||||
rulerRuleType,
|
||||
} from '../../utils/rules';
|
||||
import { createRelativeUrl } from '../../utils/url';
|
||||
|
||||
type RuleProps = RequireAtLeastOne<{
|
||||
@@ -69,6 +76,12 @@ export function RuleActionsButtons({ compact, rule, promRule, groupIdentifier }:
|
||||
return null;
|
||||
}
|
||||
|
||||
// determine if this rule can be silenced by checking for Grafana Alert rule type and extracting the UID
|
||||
const ruleUid = getRuleUID(rule ?? promRule);
|
||||
const silenceableRule =
|
||||
isString(ruleUid) &&
|
||||
(rulerRuleType.grafana.alertingRule(rule) || prometheusRuleType.grafana.alertingRule(promRule));
|
||||
|
||||
if (canEditRule) {
|
||||
const editURL = createRelativeUrl(`/alerting/${encodeURIComponent(ruleId.stringifyIdentifier(identifier))}/edit`);
|
||||
|
||||
@@ -96,13 +109,13 @@ export function RuleActionsButtons({ compact, rule, promRule, groupIdentifier }:
|
||||
promRule={promRule}
|
||||
groupIdentifier={groupIdentifier}
|
||||
identifier={identifier}
|
||||
handleDelete={() => showDeleteModal(identifier, groupIdentifier)}
|
||||
handleDelete={(identifier, groupIdentifier) => showDeleteModal(identifier, groupIdentifier)}
|
||||
handleSilence={() => setShowSilenceDrawer(true)}
|
||||
handleDuplicateRule={() => setRedirectToClone({ identifier, isProvisioned })}
|
||||
/>
|
||||
{deleteModal}
|
||||
{rulerRuleType.grafana.alertingRule(rule) && showSilenceDrawer && (
|
||||
<SilenceGrafanaRuleDrawer rulerRule={rule} onClose={() => setShowSilenceDrawer(false)} />
|
||||
{silenceableRule && showSilenceDrawer && (
|
||||
<SilenceGrafanaRuleDrawer ruleUid={ruleUid} onClose={() => setShowSilenceDrawer(false)} />
|
||||
)}
|
||||
{redirectToClone?.identifier && (
|
||||
<RedirectToCloneRule
|
||||
|
||||
@@ -533,3 +533,22 @@ export function isRecordingRuleByType(type?: RuleFormType) {
|
||||
export function isDataSourceManagedRuleByType(type?: RuleFormType) {
|
||||
return isCloudAlertingRuleByType(type) || isCloudRecordingRuleByType(type);
|
||||
}
|
||||
|
||||
/*
|
||||
* Grab the UID from either a rulerRule definition or a Prometheus rule definition, only Grafana-managed rules will have a UID.
|
||||
*/
|
||||
export function getRuleUID(rule?: RulerRuleDTO | Rule) {
|
||||
if (!rule) {
|
||||
return;
|
||||
}
|
||||
|
||||
let ruleUid: string | undefined;
|
||||
|
||||
if ('grafana_alert' in rule && rulerRuleType.grafana.rule(rule)) {
|
||||
ruleUid = rule.grafana_alert.uid;
|
||||
} else if ('uid' in rule && prometheusRuleType.grafana.rule(rule)) {
|
||||
ruleUid = rule.uid;
|
||||
}
|
||||
|
||||
return ruleUid;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user