mirror of https://github.com/grafana/grafana.git
				
				
				
			Alerting: Add ruleUid prop and improve test coverage (#111118)
* add isruleEditable check in per rule ui and details page * add tests * update translations * revert logic for not editable rules => we will allow adding/deleting/updating enrichments per this rule * update translations * update test * pr feedback * lint
This commit is contained in:
		
							parent
							
								
									0e7a5ffc86
								
							
						
					
					
						commit
						96f70df167
					
				|  | @ -0,0 +1,97 @@ | |||
| import { ComponentType } from 'react'; | ||||
| import { render, screen } from 'test/test-utils'; | ||||
| 
 | ||||
| import { | ||||
|   EnrichmentDrawerExtension, | ||||
|   EnrichmentDrawerExtensionProps, | ||||
|   addEnrichmentDrawerExtension, | ||||
| } from './EnrichmentDrawerExtension'; | ||||
| 
 | ||||
| // Mock component for testing
 | ||||
| const MockEnrichmentDrawer: ComponentType<EnrichmentDrawerExtensionProps> = ({ ruleUid, onClose }) => ( | ||||
|   <div data-testid="enrichment-drawer"> | ||||
|     <div data-testid="rule-uid">{ruleUid}</div> | ||||
|     <button data-testid="close-button" onClick={onClose}> | ||||
|       Close | ||||
|     </button> | ||||
|   </div> | ||||
| ); | ||||
| 
 | ||||
| describe('EnrichmentDrawerExtension', () => { | ||||
|   const mockOnClose = jest.fn(); | ||||
|   const defaultProps = { | ||||
|     ruleUid: 'test-rule-uid', | ||||
|     onClose: mockOnClose, | ||||
|   }; | ||||
| 
 | ||||
|   beforeEach(() => { | ||||
|     jest.clearAllMocks(); | ||||
|   }); | ||||
| 
 | ||||
|   it('should render nothing when no extension is registered', () => { | ||||
|     render(<EnrichmentDrawerExtension {...defaultProps} />); | ||||
| 
 | ||||
|     expect(screen.queryByTestId('enrichment-drawer')).not.toBeInTheDocument(); | ||||
|   }); | ||||
| 
 | ||||
|   it('should render registered extension with correct props', () => { | ||||
|     addEnrichmentDrawerExtension(MockEnrichmentDrawer); | ||||
| 
 | ||||
|     render(<EnrichmentDrawerExtension {...defaultProps} />); | ||||
| 
 | ||||
|     expect(screen.getByTestId('enrichment-drawer')).toBeInTheDocument(); | ||||
|     expect(screen.getByTestId('rule-uid')).toHaveTextContent('test-rule-uid'); | ||||
|   }); | ||||
| 
 | ||||
|   it('should call onClose when close button is clicked', () => { | ||||
|     addEnrichmentDrawerExtension(MockEnrichmentDrawer); | ||||
| 
 | ||||
|     render(<EnrichmentDrawerExtension {...defaultProps} />); | ||||
| 
 | ||||
|     const closeButton = screen.getByTestId('close-button'); | ||||
|     closeButton.click(); | ||||
| 
 | ||||
|     expect(mockOnClose).toHaveBeenCalledTimes(1); | ||||
|   }); | ||||
| 
 | ||||
|   it('should handle different rule UIDs', () => { | ||||
|     addEnrichmentDrawerExtension(MockEnrichmentDrawer); | ||||
| 
 | ||||
|     const differentRuleUid = 'different-rule-uid'; | ||||
|     render(<EnrichmentDrawerExtension {...defaultProps} ruleUid={differentRuleUid} />); | ||||
| 
 | ||||
|     expect(screen.getByTestId('rule-uid')).toHaveTextContent(differentRuleUid); | ||||
|   }); | ||||
| 
 | ||||
|   it('should re-render when ruleUid prop changes', () => { | ||||
|     addEnrichmentDrawerExtension(MockEnrichmentDrawer); | ||||
| 
 | ||||
|     const { rerender } = render(<EnrichmentDrawerExtension {...defaultProps} ruleUid="rule-1" />); | ||||
| 
 | ||||
|     expect(screen.getByTestId('rule-uid')).toHaveTextContent('rule-1'); | ||||
| 
 | ||||
|     rerender(<EnrichmentDrawerExtension {...defaultProps} ruleUid="rule-2" />); | ||||
| 
 | ||||
|     expect(screen.getByTestId('rule-uid')).toHaveTextContent('rule-2'); | ||||
|   }); | ||||
| 
 | ||||
|   it('should handle error boundary when extension throws', () => { | ||||
|     const consoleSpy = jest.spyOn(console, 'error').mockImplementation(() => {}); | ||||
| 
 | ||||
|     const FailingComponent: ComponentType<EnrichmentDrawerExtensionProps> = () => { | ||||
|       throw new Error('Test error'); | ||||
|     }; | ||||
| 
 | ||||
|     addEnrichmentDrawerExtension(FailingComponent); | ||||
| 
 | ||||
|     // Should not throw, error boundary should catch it
 | ||||
|     expect(() => { | ||||
|       render(<EnrichmentDrawerExtension {...defaultProps} />); | ||||
|     }).not.toThrow(); | ||||
| 
 | ||||
|     // Error boundary should render fallback UI
 | ||||
|     expect(screen.getByText(/Enrichment Drawer Extension failed to load/i)).toBeInTheDocument(); | ||||
| 
 | ||||
|     consoleSpy.mockRestore(); | ||||
|   }); | ||||
| }); | ||||
|  | @ -15,9 +15,7 @@ import { | |||
|   AlertRuleAction, | ||||
|   skipToken, | ||||
|   useGrafanaPromRuleAbilities, | ||||
|   useGrafanaPromRuleAbility, | ||||
|   useRulerRuleAbilities, | ||||
|   useRulerRuleAbility, | ||||
| } from '../../hooks/useAbilities'; | ||||
| import { createShareLink, isLocalDevEnv, isOpenSourceEdition } from '../../utils/misc'; | ||||
| import * as ruleId from '../../utils/rule-id'; | ||||
|  | @ -89,16 +87,6 @@ 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); | ||||
|  | @ -148,7 +136,6 @@ const AlertRuleMenu = ({ | |||
| 
 | ||||
|   // 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 && | ||||
|  |  | |||
|  | @ -30,6 +30,7 @@ import { stringifyIdentifier } from '../../utils/rule-id'; | |||
| 
 | ||||
| import { AlertRuleProvider } from './RuleContext'; | ||||
| import RuleViewer, { ActiveTab } from './RuleViewer'; | ||||
| import { addRulePageEnrichmentSection } from './tabs/extensions/RuleViewerExtension'; | ||||
| 
 | ||||
| // metadata and interactive elements
 | ||||
| const ELEMENTS = { | ||||
|  | @ -411,6 +412,79 @@ describe('RuleViewer', () => { | |||
|       expect(ELEMENTS.details.pendingPeriod.get()).toHaveTextContent(/15m/i); | ||||
|     }); | ||||
|   }); | ||||
| 
 | ||||
|   describe('Enrichment tab', () => { | ||||
|     const mockRule = getGrafanaRule( | ||||
|       { | ||||
|         name: 'Test alert', | ||||
|         uid: 'test-rule-uid', | ||||
|         annotations: { | ||||
|           [Annotation.summary]: 'This is the summary for the rule', | ||||
|         }, | ||||
|         labels: { | ||||
|           team: 'operations', | ||||
|           severity: 'low', | ||||
|         }, | ||||
|         group: { | ||||
|           name: 'my-group', | ||||
|           interval: '15m', | ||||
|           rules: [], | ||||
|           totals: { alerting: 1 }, | ||||
|         }, | ||||
|       }, | ||||
|       { uid: grafanaRulerRule.grafana_alert.uid } | ||||
|     ); | ||||
|     const mockRuleIdentifier = ruleId.fromCombinedRule('grafana', mockRule); | ||||
| 
 | ||||
|     beforeEach(() => { | ||||
|       grantPermissionsHelper([ | ||||
|         AccessControlAction.AlertingRuleCreate, | ||||
|         AccessControlAction.AlertingRuleRead, | ||||
|         AccessControlAction.AlertingRuleUpdate, | ||||
|         AccessControlAction.AlertingRuleDelete, | ||||
|         AccessControlAction.AlertingInstanceRead, | ||||
|         AccessControlAction.AlertingInstanceCreate, | ||||
|         AccessControlAction.AlertingInstancesExternalRead, | ||||
|         AccessControlAction.AlertingInstancesExternalWrite, | ||||
|       ]); | ||||
|     }); | ||||
| 
 | ||||
|     it('should pass correct props to enrichment section extension for editable rule', async () => { | ||||
|       const mockEnrichmentExtension = jest.fn(() => <div data-testid="enrichment-section">Enrichment Section</div>); | ||||
|       addRulePageEnrichmentSection(mockEnrichmentExtension); | ||||
| 
 | ||||
|       await renderRuleViewer(mockRule, mockRuleIdentifier, ActiveTab.Enrichment); | ||||
| 
 | ||||
|       expect(mockEnrichmentExtension).toHaveBeenCalledWith( | ||||
|         expect.objectContaining({ | ||||
|           ruleUid: 'test-rule-uid', | ||||
|         }), | ||||
|         expect.any(Object) | ||||
|       ); | ||||
|       expect(screen.getByTestId('enrichment-section')).toBeInTheDocument(); | ||||
|     }); | ||||
| 
 | ||||
|     it('should pass correct props to enrichment section extension for read-only rule', async () => { | ||||
|       grantPermissionsHelper([ | ||||
|         AccessControlAction.AlertingRuleRead, | ||||
|         AccessControlAction.AlertingInstanceRead, | ||||
|         AccessControlAction.AlertingInstancesExternalRead, | ||||
|       ]); | ||||
| 
 | ||||
|       const mockEnrichmentExtension = jest.fn(() => <div data-testid="enrichment-section">Enrichment Section</div>); | ||||
|       addRulePageEnrichmentSection(mockEnrichmentExtension); | ||||
| 
 | ||||
|       await renderRuleViewer(mockRule, mockRuleIdentifier, ActiveTab.Enrichment); | ||||
| 
 | ||||
|       expect(mockEnrichmentExtension).toHaveBeenCalledWith( | ||||
|         expect.objectContaining({ | ||||
|           ruleUid: 'test-rule-uid', | ||||
|         }), | ||||
|         expect.any(Object) | ||||
|       ); | ||||
|       expect(screen.getByTestId('enrichment-section')).toBeInTheDocument(); | ||||
|     }); | ||||
|   }); | ||||
| }); | ||||
| 
 | ||||
| const renderRuleViewer = async (rule: CombinedRule, identifier: RuleIdentifier, tab: ActiveTab = ActiveTab.Query) => { | ||||
|  |  | |||
|  | @ -174,7 +174,7 @@ const RuleViewer = () => { | |||
|           {activeTab === ActiveTab.VersionHistory && rulerRuleType.grafana.rule(rule.rulerRule) && ( | ||||
|             <AlertVersionHistory rule={rule.rulerRule} /> | ||||
|           )} | ||||
|           {activeTab === ActiveTab.Enrichment && <RulePageEnrichmentSectionExtension />} | ||||
|           {activeTab === ActiveTab.Enrichment && rule.uid && <RulePageEnrichmentSectionExtension ruleUid={rule.uid} />} | ||||
|         </TabContent> | ||||
|       </Stack> | ||||
|       {duplicateRuleIdentifier && ( | ||||
|  |  | |||
|  | @ -5,7 +5,9 @@ import { withErrorBoundary } from '@grafana/ui'; | |||
| 
 | ||||
| import { logError } from '../../../../Analytics'; | ||||
| 
 | ||||
| export interface RuleViewerExtensionProps {} | ||||
| export interface RuleViewerExtensionProps { | ||||
|   ruleUid: string; | ||||
| } | ||||
| 
 | ||||
| let InternalRulePageEnrichmentSection: ComponentType<RuleViewerExtensionProps> | null = null; | ||||
| 
 | ||||
|  |  | |||
|  | @ -1,5 +1,7 @@ | |||
| import { beforeEach, describe, expect, it } from '@jest/globals'; | ||||
| 
 | ||||
| import { addRulePageEnrichmentSection } from '../../components/rule-viewer/tabs/extensions/RuleViewerExtension'; | ||||
| 
 | ||||
| import { __clearRuleViewTabsForTests, addEnrichmentSection, getRuleViewExtensionTabs } from './extensions'; | ||||
| 
 | ||||
| describe('rule-view-page navigation', () => { | ||||
|  | @ -28,4 +30,28 @@ describe('rule-view-page navigation', () => { | |||
|     expect(enrichment).toBeTruthy(); | ||||
|     expect(enrichment!.active).toBe(true); | ||||
|   }); | ||||
| 
 | ||||
|   describe('enrichment section registration', () => { | ||||
|     it('should register enrichment section with correct prop interface', () => { | ||||
|       const mockEnrichmentSection = jest.fn(() => null); | ||||
| 
 | ||||
|       // This should not throw an error
 | ||||
|       expect(() => { | ||||
|         addRulePageEnrichmentSection(mockEnrichmentSection); | ||||
|       }).not.toThrow(); | ||||
|     }); | ||||
| 
 | ||||
|     it('should handle enrichment section with required props', () => { | ||||
|       const mockEnrichmentSection = jest.fn((props: { ruleUid: string }) => { | ||||
|         expect(props).toHaveProperty('ruleUid'); | ||||
|         expect(typeof props.ruleUid).toBe('string'); | ||||
|         return null; | ||||
|       }); | ||||
| 
 | ||||
|       addRulePageEnrichmentSection(mockEnrichmentSection); | ||||
| 
 | ||||
|       // The registration should succeed
 | ||||
|       expect(mockEnrichmentSection).toBeDefined(); | ||||
|     }); | ||||
|   }); | ||||
| }); | ||||
|  |  | |||
		Loading…
	
		Reference in New Issue