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) => {
|
queryFn: async (rulesSourceIdentifier) => {
|
||||||
const dataSourceUID = getDataSourceUID(rulesSourceIdentifier);
|
const dataSourceUID = getDataSourceUID(rulesSourceIdentifier);
|
||||||
if (!dataSourceUID) {
|
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) {
|
if (dataSourceUID === GrafanaRulesSourceSymbol) {
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ import { AccessControlAction, FolderDTO } from 'app/types';
|
|||||||
import { setupMswServer } from '../mockApi';
|
import { setupMswServer } from '../mockApi';
|
||||||
import { mockDataSource, mockFolder, mockRulerAlertingRule, mockRulerGrafanaRule } from '../mocks';
|
import { mockDataSource, mockFolder, mockRulerAlertingRule, mockRulerGrafanaRule } from '../mocks';
|
||||||
import { setupDataSources } from '../testSetup/datasources';
|
import { setupDataSources } from '../testSetup/datasources';
|
||||||
|
import { GRAFANA_RULES_SOURCE_NAME } from '../utils/datasource';
|
||||||
|
|
||||||
import { useFolder } from './useFolder';
|
import { useFolder } from './useFolder';
|
||||||
import { useIsRuleEditable } from './useIsRuleEditable';
|
import { useIsRuleEditable } from './useIsRuleEditable';
|
||||||
@@ -56,7 +57,9 @@ describe('useIsRuleEditable', () => {
|
|||||||
|
|
||||||
const wrapper = getProviderWrapper();
|
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));
|
await waitFor(() => expect(result.current.loading).toBe(false));
|
||||||
expect(result.current.isRemovable).toBe(true);
|
expect(result.current.isRemovable).toBe(true);
|
||||||
|
|||||||
@@ -3,7 +3,6 @@ import { RulerRuleDTO } from 'app/types/unified-alerting-dto';
|
|||||||
|
|
||||||
import { featureDiscoveryApi } from '../api/featureDiscoveryApi';
|
import { featureDiscoveryApi } from '../api/featureDiscoveryApi';
|
||||||
import { getRulesPermissions } from '../utils/access-control';
|
import { getRulesPermissions } from '../utils/access-control';
|
||||||
import { getDatasourceAPIUid } from '../utils/datasource';
|
|
||||||
import { isGrafanaRulerRule } from '../utils/rules';
|
import { isGrafanaRulerRule } from '../utils/rules';
|
||||||
|
|
||||||
import { useFolder } from './useFolder';
|
import { useFolder } from './useFolder';
|
||||||
@@ -13,11 +12,16 @@ interface ResultBag {
|
|||||||
isEditable?: boolean;
|
isEditable?: boolean;
|
||||||
isRemovable?: boolean;
|
isRemovable?: boolean;
|
||||||
loading: boolean;
|
loading: boolean;
|
||||||
|
error?: unknown;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function useIsRuleEditable(rulesSourceName: string, rule?: RulerRuleDTO): ResultBag {
|
export function useIsRuleEditable(rulesSourceName: string, rule?: RulerRuleDTO): ResultBag {
|
||||||
const { currentData: dsFeatures, isLoading } = featureDiscoveryApi.endpoints.discoverDsFeatures.useQuery({
|
const {
|
||||||
uid: getDatasourceAPIUid(rulesSourceName),
|
currentData: dsFeatures,
|
||||||
|
isLoading,
|
||||||
|
error,
|
||||||
|
} = featureDiscoveryApi.endpoints.discoverDsFeatures.useQuery({
|
||||||
|
rulesSourceName,
|
||||||
});
|
});
|
||||||
|
|
||||||
const folderUID = rule && isGrafanaRulerRule(rule) ? rule.grafana_alert.namespace_uid : undefined;
|
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 rulePermission = getRulesPermissions(rulesSourceName);
|
||||||
const { folder, loading } = useFolder(folderUID);
|
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) {
|
if (!rule) {
|
||||||
return { isEditable: false, isRemovable: false, loading: false };
|
return { isEditable: false, isRemovable: false, loading: false };
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -57,6 +57,7 @@ import {
|
|||||||
import { DashboardSearchItem, DashboardSearchItemType } from '../../search/types';
|
import { DashboardSearchItem, DashboardSearchItemType } from '../../search/types';
|
||||||
|
|
||||||
import { SimpleConditionIdentifier } from './components/rule-editor/query-and-alert-condition/SimpleCondition';
|
import { SimpleConditionIdentifier } from './components/rule-editor/query-and-alert-condition/SimpleCondition';
|
||||||
|
import { GRAFANA_RULES_SOURCE_NAME } from './utils/datasource';
|
||||||
import { parsePromQLStyleMatcherLooseSafe } from './utils/matchers';
|
import { parsePromQLStyleMatcherLooseSafe } from './utils/matchers';
|
||||||
|
|
||||||
let nextDataSourceId = 1;
|
let nextDataSourceId = 1;
|
||||||
@@ -686,7 +687,7 @@ export function getGrafanaRule(override?: Partial<CombinedRule>, rulerOverride?:
|
|||||||
namespace: {
|
namespace: {
|
||||||
groups: [],
|
groups: [],
|
||||||
name: 'Grafana',
|
name: 'Grafana',
|
||||||
rulesSource: 'grafana',
|
rulesSource: GRAFANA_RULES_SOURCE_NAME,
|
||||||
},
|
},
|
||||||
rulerRule: mockGrafanaRulerRule(rulerOverride),
|
rulerRule: mockGrafanaRulerRule(rulerOverride),
|
||||||
...override,
|
...override,
|
||||||
|
|||||||
@@ -17,17 +17,30 @@ interface ExistingRuleEditorProps {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function ExistingRuleEditor({ identifier, prefill }: ExistingRuleEditorProps) {
|
export function ExistingRuleEditor({ identifier, prefill }: ExistingRuleEditorProps) {
|
||||||
const {
|
|
||||||
loading: loadingAlertRule,
|
|
||||||
result: ruleWithLocation,
|
|
||||||
error,
|
|
||||||
} = useRuleWithLocation({ ruleIdentifier: identifier });
|
|
||||||
const [queryParams] = useQueryParams();
|
const [queryParams] = useQueryParams();
|
||||||
const isManualRestore = Boolean(queryParams.isManualRestore);
|
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;
|
const loading = loadingAlertRule || loadingEditable;
|
||||||
|
|
||||||
@@ -35,14 +48,6 @@ export function ExistingRuleEditor({ identifier, prefill }: ExistingRuleEditorPr
|
|||||||
return <LoadingPlaceholder text="Loading rule..." />;
|
return <LoadingPlaceholder text="Loading rule..." />;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (error) {
|
|
||||||
return (
|
|
||||||
<Alert severity="error" title="Failed to load rule">
|
|
||||||
{stringifyErrorLike(error)}
|
|
||||||
</Alert>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!ruleWithLocation && !loading) {
|
if (!ruleWithLocation && !loading) {
|
||||||
return <AlertWarning title="Rule not found">Sorry! This rule does not exist.</AlertWarning>;
|
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 { contextSrv } from 'app/core/services/context_srv';
|
||||||
import { setFolderResponse } from 'app/features/alerting/unified/mocks/server/configure';
|
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 { captureRequests } from 'app/features/alerting/unified/mocks/server/events';
|
||||||
import { DashboardSearchItemType } from 'app/features/search/types';
|
import { DashboardSearchItemType } from 'app/features/search/types';
|
||||||
import { AccessControlAction } from 'app/types';
|
import { AccessControlAction } from 'app/types';
|
||||||
@@ -12,6 +11,7 @@ import { AccessControlAction } from 'app/types';
|
|||||||
import { setupMswServer } from '../mockApi';
|
import { setupMswServer } from '../mockApi';
|
||||||
import { grantUserPermissions, mockDataSource, mockFolder } from '../mocks';
|
import { grantUserPermissions, mockDataSource, mockFolder } from '../mocks';
|
||||||
import { grafanaRulerRule } from '../mocks/grafanaRulerApi';
|
import { grafanaRulerRule } from '../mocks/grafanaRulerApi';
|
||||||
|
import { MIMIR_DATASOURCE_UID } from '../mocks/server/constants';
|
||||||
import { setupDataSources } from '../testSetup/datasources';
|
import { setupDataSources } from '../testSetup/datasources';
|
||||||
import { Annotation } from '../utils/constants';
|
import { Annotation } from '../utils/constants';
|
||||||
|
|
||||||
@@ -151,3 +151,15 @@ describe('RuleEditor grafana managed rules', () => {
|
|||||||
expect(postBody.interval).toBe('12m');
|
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