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",