Alerting: Add extension point for rule view page enrichment section (#110498)

* add enrichment to rule page view WIP

* add Alert enrichment tab to rule view page

* fix to view dummy component

* move rule view enrichment tab to enterprise

* remove better file changes

* remove console log

* add test for enrichment tab

* run yarn i18n-extract

* update directory structure

* remove .betterer.results changes

* Convert React.createElement call to JSX syntax

* revert removed lines

* revert removed lines

* revert removed lines

* fix failing test

* fix lint error

---------

Co-authored-by: Sonia Aguilar <soniaaguilarpeiron@gmail.com>
This commit is contained in:
Lauren 2025-09-09 11:33:12 +01:00 committed by GitHub
parent 4e05bb36f2
commit 53cd0882ed
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
7 changed files with 145 additions and 9 deletions

View File

@ -1723,11 +1723,6 @@
"count": 1
}
},
"public/app/features/alerting/unified/components/rule-viewer/RuleViewer.tsx": {
"react-hooks/rules-of-hooks": {
"count": 1
}
},
"public/app/features/alerting/unified/components/rules/Filter/RulesFilter.v1.tsx": {
"no-restricted-syntax": {
"count": 4

View File

@ -32,6 +32,7 @@ import { PromAlertingRuleState, PromRuleType } from 'app/types/unified-alerting-
import { logError } from '../../Analytics';
import { defaultPageNav } from '../../RuleViewer';
import { useRuleViewExtensionsNav } from '../../enterprise-components/rule-view-page/navigation';
import { shouldUseAlertingListViewV2, shouldUsePrometheusRulesPrimary } from '../../featureToggles';
import { isError, useAsync } from '../../hooks/useAsync';
import { useRuleLocation } from '../../hooks/useCombinedRule';
@ -71,6 +72,7 @@ import { History } from './tabs/History';
import { InstancesList } from './tabs/Instances';
import { QueryResults } from './tabs/Query';
import { Routing } from './tabs/Routing';
import { RulePageEnrichmentSectionExtension } from './tabs/extensions/RuleViewerExtension';
export enum ActiveTab {
Query = 'query',
@ -79,6 +81,7 @@ export enum ActiveTab {
Routing = 'routing',
Details = 'details',
VersionHistory = 'version-history',
Enrichment = 'enrichment',
}
const prometheusRulesPrimary = shouldUsePrometheusRulesPrimary();
@ -87,6 +90,7 @@ const alertingListViewV2 = shouldUseAlertingListViewV2();
const RuleViewer = () => {
const { rule, identifier } = useAlertRule();
const { pageNav, activeTab } = usePageNav(rule);
const styles = useStyles2(getStyles);
// GMA /api/v1/rules endpoint is strongly consistent, so we don't need to check for consistency
const shouldUseConsistencyCheck = isGrafanaRuleIdentifier(identifier)
@ -128,7 +132,7 @@ const RuleViewer = () => {
/>
)}
actions={<RuleActionsButtons rule={rule} rulesSource={rule.namespace.rulesSource} />}
info={createMetadata(rule)}
info={createMetadata(rule, styles)}
subTitle={
<Stack direction="column">
{summary}
@ -171,6 +175,7 @@ const RuleViewer = () => {
{activeTab === ActiveTab.VersionHistory && rulerRuleType.grafana.rule(rule.rulerRule) && (
<AlertVersionHistory rule={rule.rulerRule} />
)}
{activeTab === ActiveTab.Enrichment && <RulePageEnrichmentSectionExtension />}
</TabContent>
</Stack>
{duplicateRuleIdentifier && (
@ -185,7 +190,7 @@ const RuleViewer = () => {
);
};
const createMetadata = (rule: CombinedRule): PageInfoItem[] => {
const createMetadata = (rule: CombinedRule, styles: ReturnType<typeof getStyles>): PageInfoItem[] => {
const { labels, annotations, group, rulerRule } = rule;
const metadata: PageInfoItem[] = [];
@ -198,7 +203,6 @@ const createMetadata = (rule: CombinedRule): PageInfoItem[] => {
const hasLabels = labelsSize(labels) > 0;
const interval = group.interval;
const styles = useStyles2(getStyles);
// if the alert rule uses simplified routing, we'll show a link to the contact point
if (rulerRuleType.grafana.alertingRule(rulerRule)) {
@ -438,6 +442,12 @@ function usePageNav(rule: CombinedRule) {
const groupDetailsUrl = groups.detailsPageLink(dataSourceUID, namespaceString, groupName);
const setActiveTabFromString = (tab: string) => {
if (isValidTab(tab)) {
setActiveTab(tab);
}
};
const pageNav: NavModelItem = {
...defaultPageNav,
text: rule.name,
@ -483,6 +493,8 @@ function usePageNav(rule: CombinedRule) {
},
hideFromTabs: !isGrafanaAlertRule && !isGrafanaRecordingRule,
},
// Enterprise extensions can append additional tabs here
...useRuleViewExtensionsNav(activeTab, setActiveTabFromString),
],
parentItem: {
text: groupName,

View File

@ -0,0 +1,31 @@
import { ComponentType } from 'react';
import { t } from '@grafana/i18n';
import { withErrorBoundary } from '@grafana/ui';
import { logError } from '../../../../Analytics';
export interface RuleViewerExtensionProps {}
let InternalRulePageEnrichmentSection: ComponentType<RuleViewerExtensionProps> | null = null;
export const RulePageEnrichmentSectionExtension: ComponentType<RuleViewerExtensionProps> = (props) => {
if (!InternalRulePageEnrichmentSection) {
return null;
}
const WrappedComponent = withErrorBoundary(InternalRulePageEnrichmentSection, {
title: t(
'alerting.enrichment.error-boundary.rule-viewer-section-extension',
'Rule Viewer Enrichment Section failed to load'
),
style: 'alertbox',
errorLogger: logError,
});
return <WrappedComponent {...props} />;
};
export function addRulePageEnrichmentSection(component: ComponentType<RuleViewerExtensionProps>) {
InternalRulePageEnrichmentSection = component;
}

View File

@ -0,0 +1,58 @@
import { css } from '@emotion/css';
import { FeatureState, NavModelItem } from '@grafana/data';
import { t } from '@grafana/i18n';
import { FeatureBadge, useStyles2 } from '@grafana/ui';
type SetActiveTab = (tab: string) => void;
type RuleViewTabBuilderArgs = {
activeTab: string;
setActiveTab: SetActiveTab;
};
type RuleViewTabBuilder = (args: RuleViewTabBuilderArgs) => NavModelItem;
const ruleViewTabBuilders: RuleViewTabBuilder[] = [];
export function registerRuleViewTab(builder: RuleViewTabBuilder) {
ruleViewTabBuilders.push(builder);
}
export function getRuleViewExtensionTabs(args: RuleViewTabBuilderArgs): NavModelItem[] {
return ruleViewTabBuilders.map((builder) => builder(args));
}
export function addEnrichmentSection() {
registerRuleViewTab(({ activeTab, setActiveTab }) => {
const tabId = 'enrichment';
return {
text: t('alerting.use-page-nav.page-nav.text.enrichment', 'Alert enrichment'),
active: activeTab === tabId,
onClick: () => setActiveTab(tabId),
tabSuffix: () => <EnrichmentTabSuffix />,
};
});
}
// ONLY FOR TESTS: resets the registered tabs between tests
export function __clearRuleViewTabsForTests() {
ruleViewTabBuilders.splice(0, ruleViewTabBuilders.length);
}
function getStyles() {
return {
tabSuffix: css({
marginLeft: 8,
}),
};
}
function EnrichmentTabSuffix() {
const styles = useStyles2(getStyles);
return (
<span className={styles.tabSuffix}>
<FeatureBadge featureState={FeatureState.new} />
</span>
);
}

View File

@ -0,0 +1,31 @@
import { beforeEach, describe, expect, it } from '@jest/globals';
import { __clearRuleViewTabsForTests, addEnrichmentSection, getRuleViewExtensionTabs } from './extensions';
describe('rule-view-page navigation', () => {
beforeEach(() => {
__clearRuleViewTabsForTests();
});
it('does not include Alert enrichment tab when not registered', () => {
const tabs = getRuleViewExtensionTabs({ activeTab: 'query', setActiveTab: () => {} });
const hasEnrichment = tabs.some((t) => t.text === 'Alert enrichment');
expect(hasEnrichment).toBe(false);
});
it('includes Alert enrichment tab when registered (enterprise + toggle on)', () => {
addEnrichmentSection();
const tabs = getRuleViewExtensionTabs({ activeTab: 'query', setActiveTab: () => {} });
const enrichment = tabs.find((t) => t.text === 'Alert enrichment');
expect(enrichment).toBeTruthy();
expect(enrichment!.active).toBe(false);
});
it('marks Alert enrichment tab active when selected', () => {
addEnrichmentSection();
const tabs = getRuleViewExtensionTabs({ activeTab: 'enrichment', setActiveTab: () => {} });
const enrichment = tabs.find((t) => t.text === 'Alert enrichment');
expect(enrichment).toBeTruthy();
expect(enrichment!.active).toBe(true);
});
});

View File

@ -0,0 +1,7 @@
import { NavModelItem } from '@grafana/data';
import { getRuleViewExtensionTabs } from './extensions';
export function useRuleViewExtensionsNav(activeTab: string, setActiveTab: (tab: string) => void): NavModelItem[] {
return getRuleViewExtensionTabs({ activeTab, setActiveTab });
}

View File

@ -1121,7 +1121,8 @@
},
"enrichment": {
"error-boundary": {
"notification-message-section-extension": "Notification Message Section Extension failed to load"
"notification-message-section-extension": "Notification Message Section Extension failed to load",
"rule-viewer-section-extension": "Rule Viewer Enrichment Section failed to load"
}
},
"error-modal": {
@ -3042,6 +3043,7 @@
"page-nav": {
"text": {
"details": "Details",
"enrichment": "Alert enrichment",
"history": "History",
"instances": "Instances",
"query-and-conditions": "Query and conditions",