Alerting: Add error handling for missing data source (#101508)
This commit is contained in:
@@ -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) {
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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 };
|
||||
}
|
||||
|
||||
@@ -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<CombinedRule>, rulerOverride?:
|
||||
namespace: {
|
||||
groups: [],
|
||||
name: 'Grafana',
|
||||
rulesSource: 'grafana',
|
||||
rulesSource: GRAFANA_RULES_SOURCE_NAME,
|
||||
},
|
||||
rulerRule: mockGrafanaRulerRule(rulerOverride),
|
||||
...override,
|
||||
|
||||
@@ -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 (
|
||||
<Alert severity="error" title="Failed to load rule">
|
||||
{stringifyErrorLike(errorEditable ?? fetchRuleError)}
|
||||
</Alert>
|
||||
);
|
||||
}
|
||||
|
||||
const loading = loadingAlertRule || loadingEditable;
|
||||
|
||||
@@ -35,14 +48,6 @@ export function ExistingRuleEditor({ identifier, prefill }: ExistingRuleEditorPr
|
||||
return <LoadingPlaceholder text="Loading rule..." />;
|
||||
}
|
||||
|
||||
if (error) {
|
||||
return (
|
||||
<Alert severity="error" title="Failed to load rule">
|
||||
{stringifyErrorLike(error)}
|
||||
</Alert>
|
||||
);
|
||||
}
|
||||
|
||||
if (!ruleWithLocation && !loading) {
|
||||
return <AlertWarning title="Rule not found">Sorry! This rule does not exist.</AlertWarning>;
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user