Alerting: Enrichment per rule wip-2 (#110437)

* Add feature toggle and extension point

* Update ff name for enrichment per rule

* update translations

* wip

* remove wrong duplication after merging from main

* manage enrichments per rule drawer: add ruleUid to extension component

* remove folder

* move drawer to the list page

* update translations

* remove unused import
This commit is contained in:
Sonia Aguilar
2025-09-10 13:52:10 +02:00
committed by GitHub
parent 67d8939f0e
commit d003e54489
7 changed files with 87 additions and 37 deletions
@@ -19,7 +19,6 @@ import DashboardAnnotationField from './DashboardAnnotationField';
import { DashboardPicker, PanelDTO, getVisualPanels } from './DashboardPicker';
import { NeedHelpInfo } from './NeedHelpInfo';
import { RuleEditorSection } from './RuleEditorSection';
import { NotificationMessageSectionExtension } from './alert-rule-form/extensions/NotificationMessageSectionExtension';
import { useDashboardQuery } from './useDashboardQuery';
const AnnotationsStep = () => {
@@ -212,6 +211,7 @@ const AnnotationsStep = () => {
</div>
);
})}
<Stack direction="row" gap={1}>
<div className={styles.addAnnotationsButtonContainer}>
<Button
@@ -240,7 +240,6 @@ const AnnotationsStep = () => {
onDismiss={() => setShowPanelSelector(false)}
/>
)}
<NotificationMessageSectionExtension />
</Stack>
</RuleEditorSection>
);
@@ -1,34 +0,0 @@
import { ComponentType } from 'react';
import { t } from '@grafana/i18n';
import { withErrorBoundary } from '@grafana/ui';
import { logError } from '../../../../Analytics';
export interface NotificationMessageSectionExtensionProps {}
// Internal variable to store the extension component, for now only one component is supported
let InternalNotificationMessageSectionExtension: ComponentType<NotificationMessageSectionExtensionProps> | null = null;
// This component is used to render the notification message section extension.
export const NotificationMessageSectionExtension: ComponentType<NotificationMessageSectionExtensionProps> = (props) => {
if (!InternalNotificationMessageSectionExtension) {
return null;
}
// Wrap the component with error boundary
const WrappedComponent = withErrorBoundary(InternalNotificationMessageSectionExtension, {
title: t(
'alerting.enrichment.error-boundary.notification-message-section-extension',
'Notification Message Section Extension failed to load'
),
style: 'alertbox',
errorLogger: logError,
});
return <WrappedComponent {...props} />;
};
export function addRuleFormEnrichmentSection(component: ComponentType<NotificationMessageSectionExtensionProps>) {
InternalNotificationMessageSectionExtension = component;
}
@@ -0,0 +1,42 @@
import { ComponentType, memo } from 'react';
import { t } from '@grafana/i18n';
import { withErrorBoundary } from '@grafana/ui';
import { logError } from '../../../Analytics';
export interface EnrichmentDrawerExtensionProps {
ruleUid: string;
onClose: () => void;
}
// Internal variable to store the extension component, for now only one component is supported
let InternalEnrichmentDrawerExtension: ComponentType<EnrichmentDrawerExtensionProps> | null = null;
// This component is used to render the enrichment drawer extension.
const EnrichmentDrawerExtensionComponent: ComponentType<EnrichmentDrawerExtensionProps> = (props) => {
if (!InternalEnrichmentDrawerExtension) {
return null;
}
// Wrap the component with error boundary
const WrappedComponent = withErrorBoundary(InternalEnrichmentDrawerExtension, {
title: t(
'alerting.enrichment.error-boundary.enrichment-drawer-extension',
'Enrichment Drawer Extension failed to load'
),
style: 'alertbox',
errorLogger: logError,
});
return <WrappedComponent {...props} />;
};
export const EnrichmentDrawerExtension = memo(EnrichmentDrawerExtensionComponent, (prevProps, nextProps) => {
// Only re-render if ruleUid changes
return prevProps.ruleUid === nextProps.ruleUid;
});
export function addEnrichmentDrawerExtension(component: ComponentType<EnrichmentDrawerExtensionProps>) {
InternalEnrichmentDrawerExtension = component;
}
@@ -2,6 +2,7 @@ import { PropsOf } from '@emotion/react';
import { AppEvents } from '@grafana/data';
import { t } from '@grafana/i18n';
import { config } from '@grafana/runtime';
import { Button, ComponentSize, Dropdown, Menu } from '@grafana/ui';
import appEvents from 'app/core/app_events';
import MenuItemPauseRule from 'app/features/alerting/unified/components/MenuItemPauseRule';
@@ -14,7 +15,9 @@ import {
AlertRuleAction,
skipToken,
useGrafanaPromRuleAbilities,
useGrafanaPromRuleAbility,
useRulerRuleAbilities,
useRulerRuleAbility,
} from '../../hooks/useAbilities';
import { createShareLink, isLocalDevEnv, isOpenSourceEdition } from '../../utils/misc';
import * as ruleId from '../../utils/rule-id';
@@ -34,6 +37,7 @@ interface Props {
identifier: RuleIdentifier;
groupIdentifier: RuleGroupIdentifierV2;
handleSilence: () => void;
handleManageEnrichments?: () => void;
handleDelete: (identifier: EditableRuleIdentifier, groupIdentifier: RuleGroupIdentifierV2) => void;
handleDuplicateRule: (identifier: RuleIdentifier) => void;
onPauseChange?: () => void;
@@ -53,6 +57,7 @@ const AlertRuleMenu = ({
identifier,
groupIdentifier,
handleSilence,
handleManageEnrichments,
handleDelete,
handleDuplicateRule,
onPauseChange,
@@ -84,6 +89,16 @@ const AlertRuleMenu = ({
AlertRuleAction.ModifyExport,
]);
const [editRuleSupported, editRuleAllowed] = useRulerRuleAbility(rulerRule, groupIdentifier, AlertRuleAction.Update);
// If the consumer of this component comes from the alert list view, we need to use promRule to check abilities and permissions,
// as we have removed all requests to the ruler API in the list view.
const [grafanaEditRuleSupported, grafanaEditRuleAllowed] = useGrafanaPromRuleAbility(
prometheusRuleType.grafana.rule(promRule) ? promRule : skipToken,
AlertRuleAction.Update
);
const canEditRule = (editRuleSupported && editRuleAllowed) || (grafanaEditRuleSupported && grafanaEditRuleAllowed);
const [pauseSupported, pauseAllowed] = rulerPauseAbility;
const [grafanaPauseSupported, grafanaPauseAllowed] = grafanaPauseAbility;
const canPause = (pauseSupported && pauseAllowed) || (grafanaPauseSupported && grafanaPauseAllowed);
@@ -131,8 +146,23 @@ const AlertRuleMenu = ({
(rulerRuleType.grafana.rule(rulerRule) && isPausedRule(rulerRule)) ||
(prometheusRuleType.grafana.rule(promRule) && promRule.isPaused);
// todo: make this new menu item for enrichments an extension of the alertrulemenu items. For first iteration, we'll keep it here.
const canManageEnrichments =
canEditRule &&
ruleUid &&
handleManageEnrichments &&
config.featureToggles.alertingEnrichmentPerRule &&
config.featureToggles.alertEnrichment;
const menuItems = (
<>
{canManageEnrichments && (
<Menu.Item
label={t('alerting.alert-menu.manage-enrichments', 'Manage enrichments')}
icon="edit"
onClick={handleManageEnrichments}
/>
)}
{canPause && ruleUid && groupIdentifier.groupOrigin === 'grafana' && (
<MenuItemPauseRule
uid={ruleUid}
@@ -19,6 +19,7 @@ import { createViewLink } from '../../utils/misc';
import * as ruleId from '../../utils/rule-id';
import { getRuleUID, prometheusRuleType, rulerRuleType } from '../../utils/rules';
import { createRelativeUrl } from '../../utils/url';
import { EnrichmentDrawerExtension } from '../rule-list/extensions/EnrichmentDrawerExtension';
import { RedirectToCloneRule } from './CloneRule';
@@ -45,6 +46,7 @@ export const RuleActionsButtons = ({ compact, showViewButton, rule, rulesSource
const [deleteModal, showDeleteModal] = useDeleteModal(redirectToListView);
const [showSilenceDrawer, setShowSilenceDrawer] = useState<boolean>(false);
const [showEnrichmentDrawer, setShowEnrichmentDrawer] = useState<boolean>(false);
const [redirectToClone, setRedirectToClone] = useState<
{ identifier: RuleIdentifier; isProvisioned: boolean } | undefined
@@ -126,6 +128,7 @@ export const RuleActionsButtons = ({ compact, showViewButton, rule, rulesSource
}
}}
handleSilence={() => setShowSilenceDrawer(true)}
handleManageEnrichments={() => setShowEnrichmentDrawer(true)}
handleDuplicateRule={() => setRedirectToClone({ identifier, isProvisioned })}
onPauseChange={() => {
// Uses INSTANCES_DISPLAY_LIMIT + 1 here as exporting LIMIT_ALERTS from RuleList has the side effect
@@ -142,6 +145,9 @@ export const RuleActionsButtons = ({ compact, showViewButton, rule, rulesSource
{silenceableRule && showSilenceDrawer && (
<SilenceGrafanaRuleDrawer ruleUid={ruleUid} onClose={() => setShowSilenceDrawer(false)} />
)}
{ruleUid && showEnrichmentDrawer && (
<EnrichmentDrawerExtension ruleUid={ruleUid} onClose={() => setShowEnrichmentDrawer(false)} />
)}
{redirectToClone?.identifier && (
<RedirectToCloneRule
identifier={redirectToClone.identifier}
@@ -4,6 +4,7 @@ import { RequireAtLeastOne } from 'type-fest';
import { Trans, t } from '@grafana/i18n';
import { LinkButton, Stack } from '@grafana/ui';
import { EnrichmentDrawerExtension } from 'app/features/alerting/unified/components/rule-list/extensions/EnrichmentDrawerExtension';
import AlertRuleMenu from 'app/features/alerting/unified/components/rule-viewer/AlertRuleMenu';
import { useDeleteModal } from 'app/features/alerting/unified/components/rule-viewer/DeleteModal';
import { RedirectToCloneRule } from 'app/features/alerting/unified/components/rules/CloneRule';
@@ -50,6 +51,7 @@ export function RuleActionsButtons({ compact, rule, promRule, groupIdentifier }:
const [deleteModal, showDeleteModal] = useDeleteModal(redirectToListView);
const [showSilenceDrawer, setShowSilenceDrawer] = useState<boolean>(false);
const [showEnrichmentDrawer, setShowEnrichmentDrawer] = useState<boolean>(false);
const [redirectToClone, setRedirectToClone] = useState<
{ identifier: RuleIdentifier; isProvisioned: boolean } | undefined
@@ -111,12 +113,16 @@ export function RuleActionsButtons({ compact, rule, promRule, groupIdentifier }:
identifier={identifier}
handleDelete={(identifier, groupIdentifier) => showDeleteModal(identifier, groupIdentifier)}
handleSilence={() => setShowSilenceDrawer(true)}
handleManageEnrichments={() => setShowEnrichmentDrawer(true)}
handleDuplicateRule={() => setRedirectToClone({ identifier, isProvisioned })}
/>
{deleteModal}
{silenceableRule && showSilenceDrawer && (
<SilenceGrafanaRuleDrawer ruleUid={ruleUid} onClose={() => setShowSilenceDrawer(false)} />
)}
{ruleUid && showEnrichmentDrawer && (
<EnrichmentDrawerExtension ruleUid={ruleUid} onClose={() => setShowEnrichmentDrawer(false)} />
)}
{redirectToClone?.identifier && (
<RedirectToCloneRule
identifier={redirectToClone.identifier}
+2 -1
View File
@@ -504,6 +504,7 @@
"copy-link": "Copy link",
"duplicate": "Duplicate",
"export": "Export",
"manage-enrichments": "Manage enrichments",
"silence-notifications": "Silence notifications",
"with-modifications": "With modifications"
},
@@ -1121,7 +1122,7 @@
},
"enrichment": {
"error-boundary": {
"notification-message-section-extension": "Notification Message Section Extension failed to load",
"enrichment-drawer-extension": "Enrichment Drawer Extension failed to load",
"rule-viewer-section-extension": "Rule Viewer Enrichment Section failed to load"
}
},