diff --git a/public/app/features/alerting/unified/api/featureDiscoveryApi.ts b/public/app/features/alerting/unified/api/featureDiscoveryApi.ts index a0d40e6c874..b6d8c812163 100644 --- a/public/app/features/alerting/unified/api/featureDiscoveryApi.ts +++ b/public/app/features/alerting/unified/api/featureDiscoveryApi.ts @@ -40,7 +40,7 @@ export const featureDiscoveryApi = alertingApi.injectEndpoints({ queryFn: async (rulesSourceIdentifier) => { const dataSourceUID = getDataSourceUID(rulesSourceIdentifier); if (!dataSourceUID) { - return { error: new Error(`Unable to find data source for ${rulesSourceIdentifier}`) }; + return { error: new Error(`Unable to find data source for ${JSON.stringify(rulesSourceIdentifier)}`) }; } if (dataSourceUID === GrafanaRulesSourceSymbol) { diff --git a/public/app/features/alerting/unified/hooks/useIsRuleEditable.test.tsx b/public/app/features/alerting/unified/hooks/useIsRuleEditable.test.tsx index f8e85ba76d6..36d2d6f06d1 100644 --- a/public/app/features/alerting/unified/hooks/useIsRuleEditable.test.tsx +++ b/public/app/features/alerting/unified/hooks/useIsRuleEditable.test.tsx @@ -10,6 +10,7 @@ import { AccessControlAction, FolderDTO } from 'app/types'; import { setupMswServer } from '../mockApi'; import { mockDataSource, mockFolder, mockRulerAlertingRule, mockRulerGrafanaRule } from '../mocks'; import { setupDataSources } from '../testSetup/datasources'; +import { GRAFANA_RULES_SOURCE_NAME } from '../utils/datasource'; import { useFolder } from './useFolder'; import { useIsRuleEditable } from './useIsRuleEditable'; @@ -56,7 +57,9 @@ describe('useIsRuleEditable', () => { const wrapper = getProviderWrapper(); - const { result } = renderHook(() => useIsRuleEditable('grafana', mockRulerGrafanaRule()), { wrapper }); + const { result } = renderHook(() => useIsRuleEditable(GRAFANA_RULES_SOURCE_NAME, mockRulerGrafanaRule()), { + wrapper, + }); await waitFor(() => expect(result.current.loading).toBe(false)); expect(result.current.isRemovable).toBe(true); diff --git a/public/app/features/alerting/unified/hooks/useIsRuleEditable.ts b/public/app/features/alerting/unified/hooks/useIsRuleEditable.ts index 50ca91b8188..017e008c02c 100644 --- a/public/app/features/alerting/unified/hooks/useIsRuleEditable.ts +++ b/public/app/features/alerting/unified/hooks/useIsRuleEditable.ts @@ -3,7 +3,6 @@ import { RulerRuleDTO } from 'app/types/unified-alerting-dto'; import { featureDiscoveryApi } from '../api/featureDiscoveryApi'; import { getRulesPermissions } from '../utils/access-control'; -import { getDatasourceAPIUid } from '../utils/datasource'; import { isGrafanaRulerRule } from '../utils/rules'; import { useFolder } from './useFolder'; @@ -13,11 +12,16 @@ interface ResultBag { isEditable?: boolean; isRemovable?: boolean; loading: boolean; + error?: unknown; } export function useIsRuleEditable(rulesSourceName: string, rule?: RulerRuleDTO): ResultBag { - const { currentData: dsFeatures, isLoading } = featureDiscoveryApi.endpoints.discoverDsFeatures.useQuery({ - uid: getDatasourceAPIUid(rulesSourceName), + const { + currentData: dsFeatures, + isLoading, + error, + } = featureDiscoveryApi.endpoints.discoverDsFeatures.useQuery({ + rulesSourceName, }); const folderUID = rule && isGrafanaRulerRule(rule) ? rule.grafana_alert.namespace_uid : undefined; @@ -25,6 +29,17 @@ export function useIsRuleEditable(rulesSourceName: string, rule?: RulerRuleDTO): const rulePermission = getRulesPermissions(rulesSourceName); const { folder, loading } = useFolder(folderUID); + // handle discovery and data source errors + if (error) { + return { + isEditable: false, + isRemovable: false, + loading: false, + isRulerAvailable: false, + error, + }; + } + if (!rule) { return { isEditable: false, isRemovable: false, loading: false }; } diff --git a/public/app/features/alerting/unified/mocks.ts b/public/app/features/alerting/unified/mocks.ts index 353a9531b9d..8e2701e0e04 100644 --- a/public/app/features/alerting/unified/mocks.ts +++ b/public/app/features/alerting/unified/mocks.ts @@ -57,6 +57,7 @@ import { import { DashboardSearchItem, DashboardSearchItemType } from '../../search/types'; import { SimpleConditionIdentifier } from './components/rule-editor/query-and-alert-condition/SimpleCondition'; +import { GRAFANA_RULES_SOURCE_NAME } from './utils/datasource'; import { parsePromQLStyleMatcherLooseSafe } from './utils/matchers'; let nextDataSourceId = 1; @@ -686,7 +687,7 @@ export function getGrafanaRule(override?: Partial, rulerOverride?: namespace: { groups: [], name: 'Grafana', - rulesSource: 'grafana', + rulesSource: GRAFANA_RULES_SOURCE_NAME, }, rulerRule: mockGrafanaRulerRule(rulerOverride), ...override, diff --git a/public/app/features/alerting/unified/rule-editor/ExistingRuleEditor.tsx b/public/app/features/alerting/unified/rule-editor/ExistingRuleEditor.tsx index 00641db1eda..3ca54fdd9aa 100644 --- a/public/app/features/alerting/unified/rule-editor/ExistingRuleEditor.tsx +++ b/public/app/features/alerting/unified/rule-editor/ExistingRuleEditor.tsx @@ -17,17 +17,30 @@ interface ExistingRuleEditorProps { } export function ExistingRuleEditor({ identifier, prefill }: ExistingRuleEditorProps) { - const { - loading: loadingAlertRule, - result: ruleWithLocation, - error, - } = useRuleWithLocation({ ruleIdentifier: identifier }); const [queryParams] = useQueryParams(); const isManualRestore = Boolean(queryParams.isManualRestore); - const ruleSourceName = ruleId.ruleIdentifierToRuleSourceName(identifier); + const { + loading: loadingAlertRule, + result: ruleWithLocation, + error: fetchRuleError, + } = useRuleWithLocation({ ruleIdentifier: identifier }); - const { isEditable, loading: loadingEditable } = useIsRuleEditable(ruleSourceName, ruleWithLocation?.rule); + const ruleSourceName = ruleId.ruleIdentifierToRuleSourceName(identifier); + const { + isEditable, + loading: loadingEditable, + error: errorEditable, + } = useIsRuleEditable(ruleSourceName, ruleWithLocation?.rule); + + // error handling for fetching rule and rule RBAC + if (fetchRuleError || errorEditable) { + return ( + + {stringifyErrorLike(errorEditable ?? fetchRuleError)} + + ); + } const loading = loadingAlertRule || loadingEditable; @@ -35,14 +48,6 @@ export function ExistingRuleEditor({ identifier, prefill }: ExistingRuleEditorPr return ; } - if (error) { - return ( - - {stringifyErrorLike(error)} - - ); - } - if (!ruleWithLocation && !loading) { return Sorry! This rule does not exist.; } diff --git a/public/app/features/alerting/unified/rule-editor/RuleEditorExisting.test.tsx b/public/app/features/alerting/unified/rule-editor/RuleEditorExisting.test.tsx index 1781ab75d2b..8f6538f60b7 100644 --- a/public/app/features/alerting/unified/rule-editor/RuleEditorExisting.test.tsx +++ b/public/app/features/alerting/unified/rule-editor/RuleEditorExisting.test.tsx @@ -4,7 +4,6 @@ import { render, screen } from 'test/test-utils'; import { contextSrv } from 'app/core/services/context_srv'; import { setFolderResponse } from 'app/features/alerting/unified/mocks/server/configure'; -import { MIMIR_DATASOURCE_UID } from 'app/features/alerting/unified/mocks/server/constants'; import { captureRequests } from 'app/features/alerting/unified/mocks/server/events'; import { DashboardSearchItemType } from 'app/features/search/types'; import { AccessControlAction } from 'app/types'; @@ -12,6 +11,7 @@ import { AccessControlAction } from 'app/types'; import { setupMswServer } from '../mockApi'; import { grantUserPermissions, mockDataSource, mockFolder } from '../mocks'; import { grafanaRulerRule } from '../mocks/grafanaRulerApi'; +import { MIMIR_DATASOURCE_UID } from '../mocks/server/constants'; import { setupDataSources } from '../testSetup/datasources'; import { Annotation } from '../utils/constants'; @@ -151,3 +151,15 @@ describe('RuleEditor grafana managed rules', () => { expect(postBody.interval).toBe('12m'); }); }); + +describe('Data source managed rules', () => { + beforeEach(() => { + jest.clearAllMocks(); + grantUserPermissions([AccessControlAction.AlertingRuleExternalRead, AccessControlAction.AlertingRuleExternalWrite]); + }); + + it('should show an error if the data source does not exist', async () => { + renderRuleEditor('cri%24grafana-cloudd%24delete me%24delete me 3%24recording_rule_delete_2%24-476183141'); + expect(await screen.findByText(/unable to find data source/i)).toBeInTheDocument(); + }); +});