diff --git a/eslint-suppressions.json b/eslint-suppressions.json
index 15c6405d61d..17967697f4e 100644
--- a/eslint-suppressions.json
+++ b/eslint-suppressions.json
@@ -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
diff --git a/public/app/features/alerting/unified/components/rule-viewer/RuleViewer.tsx b/public/app/features/alerting/unified/components/rule-viewer/RuleViewer.tsx
index 1d8f0e2ea5d..5224756b624 100644
--- a/public/app/features/alerting/unified/components/rule-viewer/RuleViewer.tsx
+++ b/public/app/features/alerting/unified/components/rule-viewer/RuleViewer.tsx
@@ -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={}
- info={createMetadata(rule)}
+ info={createMetadata(rule, styles)}
subTitle={
{summary}
@@ -171,6 +175,7 @@ const RuleViewer = () => {
{activeTab === ActiveTab.VersionHistory && rulerRuleType.grafana.rule(rule.rulerRule) && (
)}
+ {activeTab === ActiveTab.Enrichment && }
{duplicateRuleIdentifier && (
@@ -185,7 +190,7 @@ const RuleViewer = () => {
);
};
-const createMetadata = (rule: CombinedRule): PageInfoItem[] => {
+const createMetadata = (rule: CombinedRule, styles: ReturnType): 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,
diff --git a/public/app/features/alerting/unified/components/rule-viewer/tabs/extensions/RuleViewerExtension.tsx b/public/app/features/alerting/unified/components/rule-viewer/tabs/extensions/RuleViewerExtension.tsx
new file mode 100644
index 00000000000..a1a5a1739e0
--- /dev/null
+++ b/public/app/features/alerting/unified/components/rule-viewer/tabs/extensions/RuleViewerExtension.tsx
@@ -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 | null = null;
+
+export const RulePageEnrichmentSectionExtension: ComponentType = (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 ;
+};
+
+export function addRulePageEnrichmentSection(component: ComponentType) {
+ InternalRulePageEnrichmentSection = component;
+}
diff --git a/public/app/features/alerting/unified/enterprise-components/rule-view-page/extensions.tsx b/public/app/features/alerting/unified/enterprise-components/rule-view-page/extensions.tsx
new file mode 100644
index 00000000000..d26e996686c
--- /dev/null
+++ b/public/app/features/alerting/unified/enterprise-components/rule-view-page/extensions.tsx
@@ -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: () => ,
+ };
+ });
+}
+
+// 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 (
+
+
+
+ );
+}
diff --git a/public/app/features/alerting/unified/enterprise-components/rule-view-page/navigation.test.ts b/public/app/features/alerting/unified/enterprise-components/rule-view-page/navigation.test.ts
new file mode 100644
index 00000000000..1801fd4e772
--- /dev/null
+++ b/public/app/features/alerting/unified/enterprise-components/rule-view-page/navigation.test.ts
@@ -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);
+ });
+});
diff --git a/public/app/features/alerting/unified/enterprise-components/rule-view-page/navigation.ts b/public/app/features/alerting/unified/enterprise-components/rule-view-page/navigation.ts
new file mode 100644
index 00000000000..2f1f4d0d5eb
--- /dev/null
+++ b/public/app/features/alerting/unified/enterprise-components/rule-view-page/navigation.ts
@@ -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 });
+}
diff --git a/public/locales/en-US/grafana.json b/public/locales/en-US/grafana.json
index 35074387140..3d0b066d978 100644
--- a/public/locales/en-US/grafana.json
+++ b/public/locales/en-US/grafana.json
@@ -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",