mirror of
https://github.com/grafana/grafana.git
synced 2025-02-25 18:55:37 -06:00
Alerting: Use new endpoints to fetch single GMA rule on view and edit pages (#87625)
This commit is contained in:
parent
b07b6771e8
commit
93870c1cd8
@ -1,17 +1,16 @@
|
||||
import { render, waitFor, waitForElementToBeRemoved, within } from '@testing-library/react';
|
||||
import { setupServer } from 'msw/node';
|
||||
import React from 'react';
|
||||
import { FormProvider, useForm } from 'react-hook-form';
|
||||
import { TestProvider } from 'test/helpers/TestProvider';
|
||||
import { byRole, byTestId, byText } from 'testing-library-selector';
|
||||
|
||||
import { selectors } from '@grafana/e2e-selectors/src';
|
||||
import { config, setBackendSrv, setDataSourceSrv } from '@grafana/runtime';
|
||||
import { backendSrv } from 'app/core/services/backend_srv';
|
||||
import { setDataSourceSrv } from '@grafana/runtime';
|
||||
import { DashboardSearchItem, DashboardSearchItemType } from 'app/features/search/types';
|
||||
import { AlertManagerCortexConfig } from 'app/plugins/datasource/alertmanager/types';
|
||||
import { RuleWithLocation } from 'app/types/unified-alerting';
|
||||
|
||||
import { AccessControlAction } from '../../../types';
|
||||
import {
|
||||
RulerAlertingRuleDTO,
|
||||
RulerGrafanaRuleDTO,
|
||||
@ -21,19 +20,21 @@ import {
|
||||
|
||||
import { cloneRuleDefinition, CloneRuleEditor } from './CloneRuleEditor';
|
||||
import { ExpressionEditorProps } from './components/rule-editor/ExpressionEditor';
|
||||
import { mockApi, mockSearchApi } from './mockApi';
|
||||
import { mockFeatureDiscoveryApi, mockSearchApi, setupMswServer } from './mockApi';
|
||||
import {
|
||||
labelsPluginMetaMock,
|
||||
grantUserPermissions,
|
||||
mockDataSource,
|
||||
MockDataSourceSrv,
|
||||
mockRulerAlertingRule,
|
||||
mockRulerGrafanaRule,
|
||||
mockRulerRuleGroup,
|
||||
mockStore,
|
||||
} from './mocks';
|
||||
import { grafanaRulerRule } from './mocks/alertRuleApi';
|
||||
import { mockAlertmanagerConfigResponse } from './mocks/alertmanagerApi';
|
||||
import { mockRulerRulesApiResponse, mockRulerRulesGroupApiResponse } from './mocks/rulerApi';
|
||||
import { AlertingQueryRunner } from './state/AlertingQueryRunner';
|
||||
import { setupDataSources } from './testSetup/datasources';
|
||||
import { buildInfoResponse } from './testSetup/featureDiscovery';
|
||||
import { RuleFormValues } from './types/rule-form';
|
||||
import { Annotation } from './utils/constants';
|
||||
import { GRAFANA_RULES_SOURCE_NAME } from './utils/datasource';
|
||||
@ -55,20 +56,7 @@ jest.mock('./components/rule-editor/notificaton-preview/NotificationPreview', ()
|
||||
|
||||
jest.spyOn(AlertingQueryRunner.prototype, 'run').mockImplementation(() => Promise.resolve());
|
||||
|
||||
const server = setupServer();
|
||||
|
||||
beforeAll(() => {
|
||||
setBackendSrv(backendSrv);
|
||||
server.listen({ onUnhandledRequest: 'error' });
|
||||
});
|
||||
|
||||
beforeEach(() => {
|
||||
server.resetHandlers();
|
||||
});
|
||||
|
||||
afterAll(() => {
|
||||
server.close();
|
||||
});
|
||||
const server = setupMswServer();
|
||||
|
||||
const ui = {
|
||||
inputs: {
|
||||
@ -80,46 +68,16 @@ const ui = {
|
||||
annotationValue: (idx: number) => byTestId(`annotation-value-${idx}`),
|
||||
labelValue: (idx: number) => byTestId(`label-value-${idx}`),
|
||||
},
|
||||
loadingIndicator: byText('Loading the rule'),
|
||||
loadingIndicator: byText('Loading the rule...'),
|
||||
};
|
||||
|
||||
function getProvidersWrapper() {
|
||||
return function Wrapper({ children }: React.PropsWithChildren<{}>) {
|
||||
const store = mockStore((store) => {
|
||||
store.unifiedAlerting.dataSources['grafana'] = {
|
||||
loading: false,
|
||||
dispatched: true,
|
||||
result: {
|
||||
id: 'grafana',
|
||||
name: 'grafana',
|
||||
rulerConfig: {
|
||||
dataSourceName: 'grafana',
|
||||
apiVersion: 'legacy',
|
||||
},
|
||||
},
|
||||
};
|
||||
store.unifiedAlerting.dataSources['my-prom-ds'] = {
|
||||
loading: false,
|
||||
dispatched: true,
|
||||
result: {
|
||||
id: 'my-prom-ds',
|
||||
name: 'my-prom-ds',
|
||||
rulerConfig: {
|
||||
dataSourceName: 'my-prom-ds',
|
||||
apiVersion: 'config',
|
||||
},
|
||||
},
|
||||
};
|
||||
});
|
||||
|
||||
const formApi = useForm<RuleFormValues>({ defaultValues: getDefaultFormValues() });
|
||||
|
||||
return (
|
||||
<TestProvider store={store}>
|
||||
<FormProvider {...formApi}>{children}</FormProvider>
|
||||
</TestProvider>
|
||||
);
|
||||
};
|
||||
function Wrapper({ children }: React.PropsWithChildren<{}>) {
|
||||
const formApi = useForm<RuleFormValues>({ defaultValues: getDefaultFormValues() });
|
||||
return (
|
||||
<TestProvider>
|
||||
<FormProvider {...formApi}>{children}</FormProvider>
|
||||
</TestProvider>
|
||||
);
|
||||
}
|
||||
|
||||
const amConfig: AlertManagerCortexConfig = {
|
||||
@ -139,33 +97,26 @@ const amConfig: AlertManagerCortexConfig = {
|
||||
template_files: {},
|
||||
};
|
||||
|
||||
mockApi(server).plugins.getPluginSettings({ ...labelsPluginMetaMock, enabled: false });
|
||||
describe('CloneRuleEditor', function () {
|
||||
grantUserPermissions([AccessControlAction.AlertingRuleExternalRead]);
|
||||
|
||||
describe('Grafana-managed rules', function () {
|
||||
it('should populate form values from the existing alert rule', async function () {
|
||||
setDataSourceSrv(new MockDataSourceSrv({}));
|
||||
|
||||
const originRule: RulerGrafanaRuleDTO = mockRulerGrafanaRule(
|
||||
{
|
||||
for: '1m',
|
||||
labels: { severity: 'critical', region: 'nasa' },
|
||||
annotations: { [Annotation.summary]: 'This is a very important alert rule' },
|
||||
},
|
||||
{ uid: 'grafana-rule-1', title: 'First Grafana Rule', data: [] }
|
||||
);
|
||||
|
||||
mockRulerRulesApiResponse(server, 'grafana', {
|
||||
'folder-one': [{ name: 'group1', interval: '20s', rules: [originRule] }],
|
||||
});
|
||||
|
||||
mockSearchApi(server).search([
|
||||
mockDashboardSearchItem({ title: 'folder-one', uid: '123', type: DashboardSearchItemType.DashDB }),
|
||||
mockDashboardSearchItem({
|
||||
title: 'folder-one',
|
||||
uid: grafanaRulerRule.grafana_alert.namespace_uid,
|
||||
type: DashboardSearchItemType.DashDB,
|
||||
}),
|
||||
]);
|
||||
mockAlertmanagerConfigResponse(server, GRAFANA_RULES_SOURCE_NAME, amConfig);
|
||||
|
||||
render(<CloneRuleEditor sourceRuleId={{ uid: 'grafana-rule-1', ruleSourceName: 'grafana' }} />, {
|
||||
wrapper: getProvidersWrapper(),
|
||||
});
|
||||
render(
|
||||
<CloneRuleEditor sourceRuleId={{ uid: grafanaRulerRule.grafana_alert.uid, ruleSourceName: 'grafana' }} />,
|
||||
{ wrapper: Wrapper }
|
||||
);
|
||||
|
||||
await waitForElementToBeRemoved(ui.loadingIndicator.query());
|
||||
await waitFor(() => {
|
||||
@ -173,9 +124,9 @@ describe('CloneRuleEditor', function () {
|
||||
});
|
||||
|
||||
await waitFor(() => {
|
||||
expect(ui.inputs.name.get()).toHaveValue('First Grafana Rule (copy)');
|
||||
expect(ui.inputs.name.get()).toHaveValue(`${grafanaRulerRule.grafana_alert.title} (copy)`);
|
||||
expect(ui.inputs.folderContainer.get()).toHaveTextContent('folder-one');
|
||||
expect(ui.inputs.group.get()).toHaveTextContent('group1');
|
||||
expect(ui.inputs.group.get()).toHaveTextContent(grafanaRulerRule.grafana_alert.rule_group);
|
||||
expect(
|
||||
byRole('listitem', {
|
||||
name: 'severity: critical',
|
||||
@ -186,7 +137,7 @@ describe('CloneRuleEditor', function () {
|
||||
name: 'region: nasa',
|
||||
}).get()
|
||||
).toBeInTheDocument();
|
||||
expect(ui.inputs.annotationValue(0).get()).toHaveTextContent('This is a very important alert rule');
|
||||
expect(ui.inputs.annotationValue(0).get()).toHaveTextContent(grafanaRulerRule.annotations[Annotation.summary]);
|
||||
});
|
||||
});
|
||||
});
|
||||
@ -197,12 +148,8 @@ describe('CloneRuleEditor', function () {
|
||||
name: 'my-prom-ds',
|
||||
uid: 'my-prom-ds',
|
||||
});
|
||||
config.datasources = {
|
||||
'my-prom-ds': dsSettings,
|
||||
};
|
||||
|
||||
setDataSourceSrv(new MockDataSourceSrv({ 'my-prom-ds': dsSettings }));
|
||||
|
||||
setupDataSources(dsSettings);
|
||||
mockFeatureDiscoveryApi(server).discoverDsFeatures(dsSettings, buildInfoResponse.mimir);
|
||||
const originRule = mockRulerAlertingRule({
|
||||
for: '1m',
|
||||
alert: 'First Ruler Rule',
|
||||
@ -242,9 +189,7 @@ describe('CloneRuleEditor', function () {
|
||||
rulerRuleHash: hashRulerRule(originRule),
|
||||
}}
|
||||
/>,
|
||||
{
|
||||
wrapper: getProvidersWrapper(),
|
||||
}
|
||||
{ wrapper: Wrapper }
|
||||
);
|
||||
|
||||
await waitForElementToBeRemoved(ui.loadingIndicator.query());
|
||||
|
@ -1,32 +1,25 @@
|
||||
import { cloneDeep } from 'lodash';
|
||||
import React from 'react';
|
||||
import { useAsync } from 'react-use';
|
||||
|
||||
import { locationService } from '@grafana/runtime/src';
|
||||
import { Alert, LoadingPlaceholder } from '@grafana/ui/src';
|
||||
|
||||
import { useDispatch } from '../../../types';
|
||||
import { RuleIdentifier, RuleWithLocation } from '../../../types/unified-alerting';
|
||||
import { RulerRuleDTO } from '../../../types/unified-alerting-dto';
|
||||
|
||||
import { AlertRuleForm } from './components/rule-editor/alert-rule-form/AlertRuleForm';
|
||||
import { fetchEditableRuleAction } from './state/actions';
|
||||
import { useRuleWithLocation } from './hooks/useCombinedRule';
|
||||
import { generateCopiedName } from './utils/duplicate';
|
||||
import { stringifyErrorLike } from './utils/misc';
|
||||
import { rulerRuleToFormValues } from './utils/rule-form';
|
||||
import { getRuleName, isAlertingRulerRule, isGrafanaRulerRule, isRecordingRulerRule } from './utils/rules';
|
||||
import { createUrl } from './utils/url';
|
||||
|
||||
export function CloneRuleEditor({ sourceRuleId }: { sourceRuleId: RuleIdentifier }) {
|
||||
const dispatch = useDispatch();
|
||||
|
||||
const {
|
||||
loading,
|
||||
value: rule,
|
||||
error,
|
||||
} = useAsync(() => dispatch(fetchEditableRuleAction(sourceRuleId)).unwrap(), [sourceRuleId]);
|
||||
const { loading, result: rule, error } = useRuleWithLocation({ ruleIdentifier: sourceRuleId });
|
||||
|
||||
if (loading) {
|
||||
return <LoadingPlaceholder text="Loading the rule" />;
|
||||
return <LoadingPlaceholder text="Loading the rule..." />;
|
||||
}
|
||||
|
||||
if (rule) {
|
||||
@ -39,7 +32,7 @@ export function CloneRuleEditor({ sourceRuleId }: { sourceRuleId: RuleIdentifier
|
||||
if (error) {
|
||||
return (
|
||||
<Alert title="Error" severity="error">
|
||||
{error.message}
|
||||
{stringifyErrorLike(error)}
|
||||
</Alert>
|
||||
);
|
||||
}
|
||||
|
@ -1,16 +1,13 @@
|
||||
import React, { useEffect } from 'react';
|
||||
import React from 'react';
|
||||
|
||||
import { Alert, LoadingPlaceholder } from '@grafana/ui';
|
||||
import { useCleanup } from 'app/core/hooks/useCleanup';
|
||||
import { useDispatch } from 'app/types';
|
||||
import { RuleIdentifier } from 'app/types/unified-alerting';
|
||||
|
||||
import { AlertWarning } from './AlertWarning';
|
||||
import { AlertRuleForm } from './components/rule-editor/alert-rule-form/AlertRuleForm';
|
||||
import { useRuleWithLocation } from './hooks/useCombinedRule';
|
||||
import { useIsRuleEditable } from './hooks/useIsRuleEditable';
|
||||
import { useUnifiedAlertingSelector } from './hooks/useUnifiedAlertingSelector';
|
||||
import { fetchEditableRuleAction } from './state/actions';
|
||||
import { initialAsyncRequestState } from './utils/redux';
|
||||
import { stringifyErrorLike } from './utils/misc';
|
||||
import * as ruleId from './utils/rule-id';
|
||||
|
||||
interface ExistingRuleEditorProps {
|
||||
@ -19,42 +16,31 @@ interface ExistingRuleEditorProps {
|
||||
}
|
||||
|
||||
export function ExistingRuleEditor({ identifier, id }: ExistingRuleEditorProps) {
|
||||
useCleanup((state) => (state.unifiedAlerting.ruleForm.existingRule = initialAsyncRequestState));
|
||||
|
||||
const {
|
||||
loading: loadingAlertRule,
|
||||
result,
|
||||
result: ruleWithLocation,
|
||||
error,
|
||||
dispatched,
|
||||
} = useUnifiedAlertingSelector((state) => state.ruleForm.existingRule);
|
||||
} = useRuleWithLocation({ ruleIdentifier: identifier });
|
||||
|
||||
const dispatch = useDispatch();
|
||||
const { isEditable, loading: loadingEditable } = useIsRuleEditable(
|
||||
ruleId.ruleIdentifierToRuleSourceName(identifier),
|
||||
result?.rule
|
||||
);
|
||||
const ruleSourceName = ruleId.ruleIdentifierToRuleSourceName(identifier);
|
||||
|
||||
const { isEditable, loading: loadingEditable } = useIsRuleEditable(ruleSourceName, ruleWithLocation?.rule);
|
||||
|
||||
const loading = loadingAlertRule || loadingEditable;
|
||||
|
||||
useEffect(() => {
|
||||
if (!dispatched) {
|
||||
dispatch(fetchEditableRuleAction(identifier));
|
||||
}
|
||||
}, [dispatched, dispatch, identifier]);
|
||||
|
||||
if (loading || isEditable === undefined) {
|
||||
if (loading) {
|
||||
return <LoadingPlaceholder text="Loading rule..." />;
|
||||
}
|
||||
|
||||
if (error) {
|
||||
return (
|
||||
<Alert severity="error" title="Failed to load rule">
|
||||
{error.message}
|
||||
{stringifyErrorLike(error)}
|
||||
</Alert>
|
||||
);
|
||||
}
|
||||
|
||||
if (!result) {
|
||||
if (!ruleWithLocation) {
|
||||
return <AlertWarning title="Rule not found">Sorry! This rule does not exist.</AlertWarning>;
|
||||
}
|
||||
|
||||
@ -62,5 +48,5 @@ export function ExistingRuleEditor({ identifier, id }: ExistingRuleEditorProps)
|
||||
return <AlertWarning title="Cannot edit rule">Sorry! You do not have permission to edit this rule.</AlertWarning>;
|
||||
}
|
||||
|
||||
return <AlertRuleForm existing={result} />;
|
||||
return <AlertRuleForm existing={ruleWithLocation} />;
|
||||
}
|
||||
|
@ -5,27 +5,25 @@ import { Route } from 'react-router-dom';
|
||||
import { TestProvider } from 'test/helpers/TestProvider';
|
||||
import { ui } from 'test/helpers/alertingRuleEditor';
|
||||
|
||||
import { locationService, setDataSourceSrv } from '@grafana/runtime';
|
||||
import { locationService } from '@grafana/runtime';
|
||||
import { contextSrv } from 'app/core/services/context_srv';
|
||||
import { DashboardSearchHit, DashboardSearchItemType } from 'app/features/search/types';
|
||||
import { GrafanaAlertStateDecision } from 'app/types/unified-alerting-dto';
|
||||
|
||||
import { searchFolders } from '../../../../app/features/manage-dashboards/state/actions';
|
||||
import { backendSrv } from '../../../core/services/backend_srv';
|
||||
import { AccessControlAction } from '../../../types';
|
||||
|
||||
import RuleEditor from './RuleEditor';
|
||||
import { discoverFeatures } from './api/buildInfo';
|
||||
import { fetchRulerRules, fetchRulerRulesGroup, fetchRulerRulesNamespace, setRulerRuleGroup } from './api/ruler';
|
||||
import * as ruler from './api/ruler';
|
||||
import { ExpressionEditorProps } from './components/rule-editor/ExpressionEditor';
|
||||
import { MockDataSourceSrv, grantUserPermissions, mockDataSource, mockFolder } from './mocks';
|
||||
import { fetchRulerRulesIfNotFetchedYet } from './state/actions';
|
||||
import * as config from './utils/config';
|
||||
import { setupMswServer } from './mockApi';
|
||||
import { grantUserPermissions, mockDataSource, mockFolder } from './mocks';
|
||||
import { grafanaRulerGroup, grafanaRulerRule } from './mocks/alertRuleApi';
|
||||
import { setupDataSources } from './testSetup/datasources';
|
||||
import { Annotation } from './utils/constants';
|
||||
import { GRAFANA_RULES_SOURCE_NAME } from './utils/datasource';
|
||||
import { getDefaultQueries } from './utils/rule-form';
|
||||
|
||||
jest.mock('./components/rule-editor/ExpressionEditor', () => ({
|
||||
// eslint-disable-next-line react/display-name
|
||||
ExpressionEditor: ({ value, onChange }: ExpressionEditorProps) => (
|
||||
<input value={value} data-testid="expr" onChange={(e) => onChange(e.target.value)} />
|
||||
),
|
||||
@ -35,34 +33,25 @@ jest.mock('app/core/components/AppChrome/AppChromeUpdate', () => ({
|
||||
AppChromeUpdate: ({ actions }: { actions: React.ReactNode }) => <div>{actions}</div>,
|
||||
}));
|
||||
|
||||
jest.mock('./api/buildInfo');
|
||||
jest.mock('./api/ruler');
|
||||
jest.mock('../../../../app/features/manage-dashboards/state/actions');
|
||||
|
||||
// there's no angular scope in test and things go terribly wrong when trying to render the query editor row.
|
||||
// lets just skip it
|
||||
jest.mock('app/features/query/components/QueryEditorRow', () => ({
|
||||
// eslint-disable-next-line react/display-name
|
||||
QueryEditorRow: () => <p>hi</p>,
|
||||
}));
|
||||
|
||||
jest.spyOn(config, 'getAllDataSources');
|
||||
|
||||
jest.setTimeout(60 * 1000);
|
||||
|
||||
const mocks = {
|
||||
getAllDataSources: jest.mocked(config.getAllDataSources),
|
||||
searchFolders: jest.mocked(searchFolders),
|
||||
api: {
|
||||
discoverFeatures: jest.mocked(discoverFeatures),
|
||||
fetchRulerRulesGroup: jest.mocked(fetchRulerRulesGroup),
|
||||
setRulerRuleGroup: jest.mocked(setRulerRuleGroup),
|
||||
fetchRulerRulesNamespace: jest.mocked(fetchRulerRulesNamespace),
|
||||
fetchRulerRules: jest.mocked(fetchRulerRules),
|
||||
fetchRulerRulesIfNotFetchedYet: jest.mocked(fetchRulerRulesIfNotFetchedYet),
|
||||
setRulerRuleGroup: jest.spyOn(ruler, 'setRulerRuleGroup'),
|
||||
},
|
||||
};
|
||||
|
||||
setupMswServer();
|
||||
|
||||
function renderRuleEditor(identifier?: string) {
|
||||
locationService.push(identifier ? `/alerting/${identifier}/edit` : `/alerting/new`);
|
||||
|
||||
@ -95,10 +84,9 @@ describe('RuleEditor grafana managed rules', () => {
|
||||
});
|
||||
|
||||
it('can edit grafana managed rule', async () => {
|
||||
const uid = 'FOOBAR123';
|
||||
const folder = {
|
||||
title: 'Folder A',
|
||||
uid: 'abcd',
|
||||
uid: grafanaRulerRule.grafana_alert.namespace_uid,
|
||||
id: 1,
|
||||
type: DashboardSearchItemType.DashDB,
|
||||
};
|
||||
@ -127,46 +115,20 @@ describe('RuleEditor grafana managed rules', () => {
|
||||
},
|
||||
});
|
||||
|
||||
setDataSourceSrv(new MockDataSourceSrv(dataSources));
|
||||
setupDataSources(dataSources.default);
|
||||
|
||||
mocks.getAllDataSources.mockReturnValue(Object.values(dataSources));
|
||||
mocks.api.setRulerRuleGroup.mockResolvedValue();
|
||||
mocks.api.fetchRulerRulesNamespace.mockResolvedValue([]);
|
||||
mocks.api.fetchRulerRules.mockResolvedValue({
|
||||
[folder.title]: [
|
||||
{
|
||||
interval: '1m',
|
||||
name: 'group1',
|
||||
rules: [
|
||||
{
|
||||
annotations: { description: 'some description', summary: 'some summary' },
|
||||
labels: { severity: 'warn', team: 'the a-team' },
|
||||
for: '1m',
|
||||
grafana_alert: {
|
||||
uid,
|
||||
namespace_uid: 'abcd',
|
||||
condition: 'B',
|
||||
data: getDefaultQueries(),
|
||||
exec_err_state: GrafanaAlertStateDecision.Error,
|
||||
no_data_state: GrafanaAlertStateDecision.NoData,
|
||||
title: 'my great new rule',
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
});
|
||||
// mocks.api.fetchRulerRulesNamespace.mockResolvedValue([]);
|
||||
mocks.searchFolders.mockResolvedValue([folder, slashedFolder] as DashboardSearchHit[]);
|
||||
|
||||
renderRuleEditor(uid);
|
||||
renderRuleEditor(grafanaRulerRule.grafana_alert.uid);
|
||||
|
||||
// check that it's filled in
|
||||
const nameInput = await ui.inputs.name.find();
|
||||
expect(nameInput).toHaveValue('my great new rule');
|
||||
expect(nameInput).toHaveValue(grafanaRulerRule.grafana_alert.title);
|
||||
//check that folder is in the list
|
||||
expect(ui.inputs.folder.get()).toHaveTextContent(new RegExp(folder.title));
|
||||
expect(ui.inputs.annotationValue(0).get()).toHaveValue('some summary');
|
||||
expect(ui.inputs.annotationValue(1).get()).toHaveValue('some description');
|
||||
expect(ui.inputs.annotationValue(0).get()).toHaveValue(grafanaRulerRule.annotations[Annotation.summary]);
|
||||
|
||||
//check that slashed folders are not in the list
|
||||
expect(ui.inputs.folder.get()).toHaveTextContent(new RegExp(folder.title));
|
||||
@ -195,25 +157,15 @@ describe('RuleEditor grafana managed rules', () => {
|
||||
|
||||
expect(mocks.api.setRulerRuleGroup).toHaveBeenCalledWith(
|
||||
{ dataSourceName: GRAFANA_RULES_SOURCE_NAME, apiVersion: 'legacy' },
|
||||
'abcd',
|
||||
grafanaRulerRule.grafana_alert.namespace_uid,
|
||||
{
|
||||
interval: '1m',
|
||||
name: 'group1',
|
||||
interval: grafanaRulerGroup.interval,
|
||||
name: grafanaRulerGroup.name,
|
||||
rules: [
|
||||
{
|
||||
annotations: { description: 'some description', summary: 'some summary', custom: 'value' },
|
||||
labels: { severity: 'warn', team: 'the a-team' },
|
||||
for: '1m',
|
||||
grafana_alert: {
|
||||
uid,
|
||||
condition: 'B',
|
||||
data: getDefaultQueries(),
|
||||
exec_err_state: GrafanaAlertStateDecision.Error,
|
||||
notification_settings: undefined,
|
||||
is_paused: false,
|
||||
no_data_state: 'NoData',
|
||||
title: 'my great new rule',
|
||||
},
|
||||
...grafanaRulerRule,
|
||||
annotations: { ...grafanaRulerRule.annotations, custom: 'value' },
|
||||
grafana_alert: { ...grafanaRulerRule.grafana_alert, namespace_uid: undefined, rule_group: undefined },
|
||||
},
|
||||
],
|
||||
}
|
||||
|
@ -5,33 +5,30 @@ import { renderRuleEditor, ui } from 'test/helpers/alertingRuleEditor';
|
||||
import { clickSelectOption } from 'test/helpers/selectOptionInTest';
|
||||
import { byRole } from 'testing-library-selector';
|
||||
|
||||
import { setDataSourceSrv } from '@grafana/runtime';
|
||||
import { contextSrv } from 'app/core/services/context_srv';
|
||||
import { setupMswServer } from 'app/features/alerting/unified/mockApi';
|
||||
import { DashboardSearchHit, DashboardSearchItemType } from 'app/features/search/types';
|
||||
import { AccessControlAction } from 'app/types';
|
||||
import { GrafanaAlertStateDecision, PromApplication } from 'app/types/unified-alerting-dto';
|
||||
import { GrafanaAlertStateDecision } from 'app/types/unified-alerting-dto';
|
||||
|
||||
import { searchFolders } from '../../../../app/features/manage-dashboards/state/actions';
|
||||
|
||||
import { discoverFeatures } from './api/buildInfo';
|
||||
import { fetchRulerRules, fetchRulerRulesGroup, fetchRulerRulesNamespace, setRulerRuleGroup } from './api/ruler';
|
||||
import * as ruler from './api/ruler';
|
||||
import { ExpressionEditorProps } from './components/rule-editor/ExpressionEditor';
|
||||
import { MockDataSourceSrv, grantUserPermissions, mockDataSource } from './mocks';
|
||||
import { fetchRulerRulesIfNotFetchedYet } from './state/actions';
|
||||
import { grantUserPermissions, mockDataSource } from './mocks';
|
||||
import { grafanaRulerGroup, grafanaRulerRule } from './mocks/alertRuleApi';
|
||||
import { setupDataSources } from './testSetup/datasources';
|
||||
import * as config from './utils/config';
|
||||
import { GRAFANA_RULES_SOURCE_NAME } from './utils/datasource';
|
||||
import { getDefaultQueries } from './utils/rule-form';
|
||||
|
||||
jest.mock('./components/rule-editor/ExpressionEditor', () => ({
|
||||
// eslint-disable-next-line react/display-name
|
||||
ExpressionEditor: ({ value, onChange }: ExpressionEditorProps) => (
|
||||
<input value={value} data-testid="expr" onChange={(e) => onChange(e.target.value)} />
|
||||
),
|
||||
}));
|
||||
|
||||
jest.mock('./api/buildInfo');
|
||||
jest.mock('./api/ruler');
|
||||
jest.mock('../../../../app/features/manage-dashboards/state/actions');
|
||||
|
||||
jest.mock('app/core/components/AppChrome/AppChromeUpdate', () => ({
|
||||
@ -41,7 +38,6 @@ jest.mock('app/core/components/AppChrome/AppChromeUpdate', () => ({
|
||||
// there's no angular scope in test and things go terribly wrong when trying to render the query editor row.
|
||||
// lets just skip it
|
||||
jest.mock('app/features/query/components/QueryEditorRow', () => ({
|
||||
// eslint-disable-next-line react/display-name
|
||||
QueryEditorRow: () => <p>hi</p>,
|
||||
}));
|
||||
|
||||
@ -54,11 +50,7 @@ const mocks = {
|
||||
searchFolders: jest.mocked(searchFolders),
|
||||
api: {
|
||||
discoverFeatures: jest.mocked(discoverFeatures),
|
||||
fetchRulerRulesGroup: jest.mocked(fetchRulerRulesGroup),
|
||||
setRulerRuleGroup: jest.mocked(setRulerRuleGroup),
|
||||
fetchRulerRulesNamespace: jest.mocked(fetchRulerRulesNamespace),
|
||||
fetchRulerRules: jest.mocked(fetchRulerRules),
|
||||
fetchRulerRulesIfNotFetchedYet: jest.mocked(fetchRulerRulesIfNotFetchedYet),
|
||||
setRulerRuleGroup: jest.spyOn(ruler, 'setRulerRuleGroup'),
|
||||
},
|
||||
};
|
||||
|
||||
@ -96,64 +88,13 @@ describe('RuleEditor grafana managed rules', () => {
|
||||
),
|
||||
};
|
||||
|
||||
setDataSourceSrv(new MockDataSourceSrv(dataSources));
|
||||
setupDataSources(dataSources.default);
|
||||
mocks.getAllDataSources.mockReturnValue(Object.values(dataSources));
|
||||
mocks.api.setRulerRuleGroup.mockResolvedValue();
|
||||
mocks.api.fetchRulerRulesNamespace.mockResolvedValue([]);
|
||||
mocks.api.fetchRulerRulesGroup.mockResolvedValue({
|
||||
name: 'group2',
|
||||
rules: [],
|
||||
});
|
||||
mocks.api.fetchRulerRules.mockResolvedValue({
|
||||
'Folder A': [
|
||||
{
|
||||
interval: '1m',
|
||||
name: 'group1',
|
||||
rules: [
|
||||
{
|
||||
annotations: { description: 'some description', summary: 'some summary' },
|
||||
labels: { severity: 'warn', team: 'the a-team' },
|
||||
for: '1m',
|
||||
grafana_alert: {
|
||||
uid: '23',
|
||||
namespace_uid: 'abcd',
|
||||
condition: 'B',
|
||||
data: getDefaultQueries(),
|
||||
exec_err_state: GrafanaAlertStateDecision.Error,
|
||||
no_data_state: GrafanaAlertStateDecision.NoData,
|
||||
title: 'my great new rule',
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
namespace2: [
|
||||
{
|
||||
interval: '1m',
|
||||
name: 'group1',
|
||||
rules: [
|
||||
{
|
||||
annotations: { description: 'some description', summary: 'some summary' },
|
||||
labels: { severity: 'warn', team: 'the a-team' },
|
||||
for: '1m',
|
||||
grafana_alert: {
|
||||
uid: '23',
|
||||
namespace_uid: 'b',
|
||||
condition: 'B',
|
||||
data: getDefaultQueries(),
|
||||
exec_err_state: GrafanaAlertStateDecision.Error,
|
||||
no_data_state: GrafanaAlertStateDecision.NoData,
|
||||
title: 'my great new rule',
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
});
|
||||
mocks.searchFolders.mockResolvedValue([
|
||||
{
|
||||
title: 'Folder A',
|
||||
uid: 'abcd',
|
||||
uid: grafanaRulerRule.grafana_alert.namespace_uid,
|
||||
id: 1,
|
||||
type: DashboardSearchItemType.DashDB,
|
||||
},
|
||||
@ -171,12 +112,6 @@ describe('RuleEditor grafana managed rules', () => {
|
||||
},
|
||||
] as DashboardSearchHit[]);
|
||||
|
||||
mocks.api.discoverFeatures.mockResolvedValue({
|
||||
application: PromApplication.Prometheus,
|
||||
features: {
|
||||
rulerApiEnabled: false,
|
||||
},
|
||||
});
|
||||
renderRuleEditor();
|
||||
await waitForElementToBeRemoved(screen.getAllByTestId('Spinner'));
|
||||
|
||||
@ -186,7 +121,7 @@ describe('RuleEditor grafana managed rules', () => {
|
||||
await clickSelectOption(folderInput, 'Folder A');
|
||||
const groupInput = await ui.inputs.group.find();
|
||||
await userEvent.click(byRole('combobox').get(groupInput));
|
||||
await clickSelectOption(groupInput, 'group1');
|
||||
await clickSelectOption(groupInput, grafanaRulerGroup.name);
|
||||
await userEvent.type(ui.inputs.annotationValue(1).get(), 'some description');
|
||||
|
||||
// save and check what was sent to backend
|
||||
@ -196,11 +131,12 @@ describe('RuleEditor grafana managed rules', () => {
|
||||
// 9seg
|
||||
expect(mocks.api.setRulerRuleGroup).toHaveBeenCalledWith(
|
||||
{ dataSourceName: GRAFANA_RULES_SOURCE_NAME, apiVersion: 'legacy' },
|
||||
'abcd',
|
||||
grafanaRulerRule.grafana_alert.namespace_uid,
|
||||
{
|
||||
interval: '1m',
|
||||
name: 'group1',
|
||||
name: grafanaRulerGroup.name,
|
||||
rules: [
|
||||
grafanaRulerRule,
|
||||
{
|
||||
annotations: { description: 'some description' },
|
||||
labels: {},
|
||||
|
@ -203,6 +203,13 @@ export const alertRuleApi = alertingApi.injectEndpoints({
|
||||
providesTags: ['CombinedAlertRule'],
|
||||
}),
|
||||
|
||||
rulerNamespace: build.query<RulerRulesConfigDTO, { rulerConfig: RulerDataSourceConfig; namespace: string }>({
|
||||
query: ({ rulerConfig, namespace }) => {
|
||||
const { path, params } = rulerUrlBuilder(rulerConfig).namespace(namespace);
|
||||
return { url: path, params };
|
||||
},
|
||||
}),
|
||||
|
||||
// TODO This should be probably a separate ruler API file
|
||||
rulerRuleGroup: build.query<
|
||||
RulerRuleGroupDTO,
|
||||
@ -219,6 +226,7 @@ export const alertRuleApi = alertingApi.injectEndpoints({
|
||||
// TODO: In future, if supported in other rulers, parametrize ruler source name
|
||||
// For now, to make the consumption of this hook clearer, only support Grafana ruler
|
||||
query: ({ uid }) => ({ url: `/api/ruler/${GRAFANA_RULES_SOURCE_NAME}/api/v1/rule/${uid}` }),
|
||||
providesTags: (_result, _error, { uid }) => [{ type: 'GrafanaRulerRule', id: uid }],
|
||||
}),
|
||||
|
||||
exportRules: build.query<string, ExportRulesParams>({
|
||||
|
@ -43,6 +43,7 @@ export const alertingApi = createApi({
|
||||
'DataSourceSettings',
|
||||
'GrafanaLabels',
|
||||
'CombinedAlertRule',
|
||||
'GrafanaRulerRule',
|
||||
],
|
||||
endpoints: () => ({}),
|
||||
});
|
||||
|
@ -5,10 +5,10 @@ import { render, waitFor, waitForElementToBeRemoved, userEvent } from 'test/test
|
||||
import { byRole, byTestId, byText } from 'testing-library-selector';
|
||||
|
||||
import { DashboardSearchItemType } from '../../../../search/types';
|
||||
import { mockAlertRuleApi, mockExportApi, mockSearchApi, setupMswServer } from '../../mockApi';
|
||||
import { getGrafanaRule, mockDashboardSearchItem, mockDataSource } from '../../mocks';
|
||||
import { mockExportApi, mockSearchApi, setupMswServer } from '../../mockApi';
|
||||
import { mockDashboardSearchItem, mockDataSource } from '../../mocks';
|
||||
import { grafanaRulerRule } from '../../mocks/alertRuleApi';
|
||||
import { setupDataSources } from '../../testSetup/datasources';
|
||||
import { GRAFANA_RULES_SOURCE_NAME } from '../../utils/datasource';
|
||||
|
||||
import GrafanaModifyExport from './GrafanaModifyExport';
|
||||
|
||||
@ -31,7 +31,7 @@ jest.mock('@grafana/ui', () => ({
|
||||
}));
|
||||
|
||||
const ui = {
|
||||
loading: byText('Loading the rule'),
|
||||
loading: byText('Loading the rule...'),
|
||||
form: {
|
||||
nameInput: byRole('textbox', { name: 'name' }),
|
||||
folder: byTestId('folder-picker'),
|
||||
@ -66,55 +66,27 @@ const server = setupMswServer();
|
||||
describe('GrafanaModifyExport', () => {
|
||||
setupDataSources(dataSources.default);
|
||||
|
||||
const grafanaRule = getGrafanaRule(undefined, {
|
||||
uid: 'test-rule-uid',
|
||||
title: 'cpu-usage',
|
||||
namespace_uid: 'folderUID1',
|
||||
data: [
|
||||
{
|
||||
refId: 'A',
|
||||
datasourceUid: dataSources.default.uid,
|
||||
queryType: 'alerting',
|
||||
relativeTimeRange: { from: 1000, to: 2000 },
|
||||
model: {
|
||||
refId: 'A',
|
||||
expression: 'vector(1)',
|
||||
queryType: 'alerting',
|
||||
datasource: { uid: dataSources.default.uid, type: 'prometheus' },
|
||||
},
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
it('Should render edit form for the specified rule', async () => {
|
||||
mockSearchApi(server).search([
|
||||
mockDashboardSearchItem({
|
||||
title: grafanaRule.namespace.name,
|
||||
uid: 'folderUID1',
|
||||
title: grafanaRulerRule.grafana_alert.title,
|
||||
uid: grafanaRulerRule.grafana_alert.namespace_uid,
|
||||
url: '',
|
||||
tags: [],
|
||||
type: DashboardSearchItemType.DashFolder,
|
||||
}),
|
||||
]);
|
||||
mockAlertRuleApi(server).rulerRules(GRAFANA_RULES_SOURCE_NAME, {
|
||||
[grafanaRule.namespace.name]: [{ name: grafanaRule.group.name, interval: '1m', rules: [grafanaRule.rulerRule!] }],
|
||||
});
|
||||
mockAlertRuleApi(server).rulerRuleGroup(GRAFANA_RULES_SOURCE_NAME, 'folderUID1', grafanaRule.group.name, {
|
||||
name: grafanaRule.group.name,
|
||||
interval: '1m',
|
||||
rules: [grafanaRule.rulerRule!],
|
||||
});
|
||||
mockExportApi(server).modifiedExport('folderUID1', {
|
||||
mockExportApi(server).modifiedExport(grafanaRulerRule.grafana_alert.namespace_uid, {
|
||||
yaml: 'Yaml Export Content',
|
||||
json: 'Json Export Content',
|
||||
});
|
||||
|
||||
const user = userEvent.setup();
|
||||
|
||||
renderModifyExport('test-rule-uid');
|
||||
renderModifyExport(grafanaRulerRule.grafana_alert.uid);
|
||||
|
||||
await waitForElementToBeRemoved(() => ui.loading.get());
|
||||
expect(await ui.form.nameInput.find()).toHaveValue('cpu-usage');
|
||||
expect(await ui.form.nameInput.find()).toHaveValue('Grafana-rule');
|
||||
|
||||
await user.click(ui.exportButton.get());
|
||||
|
||||
|
@ -1,14 +1,13 @@
|
||||
import * as React from 'react';
|
||||
import { useEffect, useState } from 'react';
|
||||
import { useAsync } from 'react-use';
|
||||
import { useMemo } from 'react';
|
||||
|
||||
import { locationService } from '@grafana/runtime';
|
||||
import { Alert, LoadingPlaceholder } from '@grafana/ui';
|
||||
|
||||
import { GrafanaRouteComponentProps } from '../../../../../core/navigation/types';
|
||||
import { useDispatch } from '../../../../../types';
|
||||
import { RuleIdentifier } from '../../../../../types/unified-alerting';
|
||||
import { fetchEditableRuleAction, fetchRulesSourceBuildInfoAction } from '../../state/actions';
|
||||
import { useRuleWithLocation } from '../../hooks/useCombinedRule';
|
||||
import { stringifyErrorLike } from '../../utils/misc';
|
||||
import { formValuesFromExistingRule } from '../../utils/rule-form';
|
||||
import * as ruleId from '../../utils/rule-id';
|
||||
import { isGrafanaRulerRule } from '../../utils/rules';
|
||||
@ -19,79 +18,34 @@ import { ModifyExportRuleForm } from '../rule-editor/alert-rule-form/ModifyExpor
|
||||
interface GrafanaModifyExportProps extends GrafanaRouteComponentProps<{ id?: string }> {}
|
||||
|
||||
export default function GrafanaModifyExport({ match }: GrafanaModifyExportProps) {
|
||||
const dispatch = useDispatch();
|
||||
|
||||
// Get rule source build info
|
||||
const [ruleIdentifier, setRuleIdentifier] = useState<RuleIdentifier | undefined>(undefined);
|
||||
|
||||
useEffect(() => {
|
||||
const identifier = ruleId.tryParse(match.params.id, true);
|
||||
setRuleIdentifier(identifier);
|
||||
const ruleIdentifier = useMemo<RuleIdentifier | undefined>(() => {
|
||||
return ruleId.tryParse(match.params.id, true);
|
||||
}, [match.params.id]);
|
||||
|
||||
const { loading: loadingBuildInfo = true } = useAsync(async () => {
|
||||
if (ruleIdentifier) {
|
||||
await dispatch(fetchRulesSourceBuildInfoAction({ rulesSourceName: ruleIdentifier.ruleSourceName }));
|
||||
}
|
||||
}, [dispatch, ruleIdentifier]);
|
||||
|
||||
// Get rule
|
||||
const {
|
||||
loading,
|
||||
value: alertRule,
|
||||
error,
|
||||
} = useAsync(async () => {
|
||||
if (!ruleIdentifier) {
|
||||
return;
|
||||
}
|
||||
return await dispatch(fetchEditableRuleAction(ruleIdentifier)).unwrap();
|
||||
}, [ruleIdentifier, loadingBuildInfo]);
|
||||
|
||||
if (!ruleIdentifier) {
|
||||
return <div>Rule not found</div>;
|
||||
}
|
||||
|
||||
if (loading) {
|
||||
return <LoadingPlaceholder text="Loading the rule" />;
|
||||
}
|
||||
|
||||
if (error) {
|
||||
return (
|
||||
<Alert title="Cannot load modify export" severity="error">
|
||||
{error.message}
|
||||
</Alert>
|
||||
);
|
||||
}
|
||||
|
||||
if (!alertRule && !loading && !loadingBuildInfo) {
|
||||
// alert rule does not exist
|
||||
return (
|
||||
<AlertingPageWrapper isLoading={loading} navId="alert-list" pageNav={{ text: 'Modify export' }}>
|
||||
<Alert
|
||||
title="Cannot load the rule. The rule does not exist"
|
||||
buttonContent="Go back to alert list"
|
||||
onRemove={() => locationService.replace(createUrl('/alerting/list'))}
|
||||
/>
|
||||
</AlertingPageWrapper>
|
||||
);
|
||||
}
|
||||
|
||||
if (alertRule && !isGrafanaRulerRule(alertRule.rule)) {
|
||||
// alert rule exists but is not a grafana-managed rule
|
||||
return (
|
||||
<AlertingPageWrapper isLoading={loading} navId="alert-list" pageNav={{ text: 'Modify export' }}>
|
||||
<Alert
|
||||
title="This rule is not a Grafana-managed alert rule"
|
||||
buttonContent="Go back to alert list"
|
||||
onRemove={() => locationService.replace(createUrl('/alerting/list'))}
|
||||
/>
|
||||
</AlertingPageWrapper>
|
||||
<ModifyExportWrapper>
|
||||
<Alert title="Invalid rule ID" severity="error">
|
||||
The rule UID in the page URL is invalid. Please check the URL and try again.
|
||||
</Alert>
|
||||
</ModifyExportWrapper>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<ModifyExportWrapper>
|
||||
<RuleModifyExport ruleIdentifier={ruleIdentifier} />
|
||||
</ModifyExportWrapper>
|
||||
);
|
||||
}
|
||||
|
||||
interface ModifyExportWrapperProps {
|
||||
children: React.ReactNode;
|
||||
}
|
||||
|
||||
function ModifyExportWrapper({ children }: ModifyExportWrapperProps) {
|
||||
return (
|
||||
<AlertingPageWrapper
|
||||
isLoading={loading}
|
||||
navId="alert-list"
|
||||
pageNav={{
|
||||
text: 'Modify export',
|
||||
@ -99,9 +53,56 @@ export default function GrafanaModifyExport({ match }: GrafanaModifyExportProps)
|
||||
'Modify the current alert rule and export the rule definition in the format of your choice. Any changes you make will not be saved.',
|
||||
}}
|
||||
>
|
||||
{alertRule && (
|
||||
<ModifyExportRuleForm ruleForm={formValuesFromExistingRule(alertRule)} alertUid={match.params.id ?? ''} />
|
||||
)}
|
||||
{children}
|
||||
</AlertingPageWrapper>
|
||||
);
|
||||
}
|
||||
|
||||
function RuleModifyExport({ ruleIdentifier }: { ruleIdentifier: RuleIdentifier }) {
|
||||
const { loading, error, result: rulerRule } = useRuleWithLocation({ ruleIdentifier: ruleIdentifier });
|
||||
|
||||
if (loading) {
|
||||
return <LoadingPlaceholder text="Loading the rule..." />;
|
||||
}
|
||||
|
||||
if (error) {
|
||||
return (
|
||||
<Alert title="Cannot load modify export" severity="error">
|
||||
{stringifyErrorLike(error)}
|
||||
</Alert>
|
||||
);
|
||||
}
|
||||
|
||||
if (!rulerRule && !loading) {
|
||||
// alert rule does not exist
|
||||
return (
|
||||
<Alert
|
||||
title="Cannot load the rule. The rule does not exist"
|
||||
buttonContent="Go back to alert list"
|
||||
onRemove={() => locationService.replace(createUrl('/alerting/list'))}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
if (rulerRule && !isGrafanaRulerRule(rulerRule.rule)) {
|
||||
// alert rule exists but is not a grafana-managed rule
|
||||
return (
|
||||
<Alert
|
||||
title="This rule is not a Grafana-managed alert rule"
|
||||
buttonContent="Go back to alert list"
|
||||
onRemove={() => locationService.replace(createUrl('/alerting/list'))}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
if (rulerRule && isGrafanaRulerRule(rulerRule.rule)) {
|
||||
return (
|
||||
<ModifyExportRuleForm
|
||||
ruleForm={formValuesFromExistingRule(rulerRule)}
|
||||
alertUid={rulerRule.rule.grafana_alert.uid}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
return <Alert title="Unknown error" />;
|
||||
}
|
||||
|
@ -1,6 +1,6 @@
|
||||
import { css } from '@emotion/css';
|
||||
import { debounce, take, uniqueId } from 'lodash';
|
||||
import React, { useCallback, useEffect, useMemo, useState } from 'react';
|
||||
import React, { useCallback, useMemo, useState } from 'react';
|
||||
import { FormProvider, useForm, useFormContext, Controller } from 'react-hook-form';
|
||||
|
||||
import { AppEvents, GrafanaTheme2, SelectableValue } from '@grafana/data';
|
||||
@ -9,15 +9,12 @@ import { AsyncSelect, Box, Button, Field, Input, Label, Modal, Stack, Text, useS
|
||||
import appEvents from 'app/core/app_events';
|
||||
import { contextSrv } from 'app/core/services/context_srv';
|
||||
import { createFolder } from 'app/features/manage-dashboards/state/actions';
|
||||
import { AccessControlAction, useDispatch } from 'app/types';
|
||||
import { CombinedRuleGroup } from 'app/types/unified-alerting';
|
||||
import { RulerRulesConfigDTO } from 'app/types/unified-alerting-dto';
|
||||
import { AccessControlAction } from 'app/types';
|
||||
import { RulerRuleGroupDTO, RulerRulesConfigDTO } from 'app/types/unified-alerting-dto';
|
||||
|
||||
import { useCombinedRuleNamespaces } from '../../hooks/useCombinedRuleNamespaces';
|
||||
import { useUnifiedAlertingSelector } from '../../hooks/useUnifiedAlertingSelector';
|
||||
import { fetchRulerRulesAction } from '../../state/actions';
|
||||
import { alertRuleApi } from '../../api/alertRuleApi';
|
||||
import { grafanaRulerConfig } from '../../hooks/useCombinedRule';
|
||||
import { RuleFormValues } from '../../types/rule-form';
|
||||
import { GRAFANA_RULES_SOURCE_NAME } from '../../utils/datasource';
|
||||
import { DEFAULT_GROUP_EVALUATION_INTERVAL } from '../../utils/rule-form';
|
||||
import { isGrafanaRulerRule } from '../../utils/rules';
|
||||
import { ProvisioningBadge } from '../Provisioning';
|
||||
@ -30,42 +27,51 @@ import { checkForPathSeparator } from './util';
|
||||
export const MAX_GROUP_RESULTS = 1000;
|
||||
|
||||
export const useFolderGroupOptions = (folderUid: string, enableProvisionedGroups: boolean) => {
|
||||
const dispatch = useDispatch();
|
||||
|
||||
// fetch the ruler rules from the database so we can figure out what other "groups" are already defined
|
||||
// for our folders
|
||||
useEffect(() => {
|
||||
dispatch(fetchRulerRulesAction({ rulesSourceName: GRAFANA_RULES_SOURCE_NAME }));
|
||||
}, [dispatch]);
|
||||
const { isLoading: isLoadingRulerNamespace, currentData: rulerNamespace } =
|
||||
alertRuleApi.endpoints.rulerNamespace.useQuery(
|
||||
{
|
||||
namespace: folderUid,
|
||||
rulerConfig: grafanaRulerConfig,
|
||||
},
|
||||
{
|
||||
skip: !folderUid,
|
||||
refetchOnMountOrArgChange: true,
|
||||
}
|
||||
);
|
||||
|
||||
const rulerRuleRequests = useUnifiedAlertingSelector((state) => state.rulerRules);
|
||||
const groupfoldersForGrafana = rulerRuleRequests[GRAFANA_RULES_SOURCE_NAME];
|
||||
// There should be only one entry in the rulerNamespace object
|
||||
// However it uses folder name as key, so to avoid fetching folder name, we use Object.values
|
||||
const groupOptions = useMemo(() => {
|
||||
if (!rulerNamespace) {
|
||||
// still waiting for namespace information to be fetched
|
||||
return [];
|
||||
}
|
||||
|
||||
const grafanaFolders = useCombinedRuleNamespaces(GRAFANA_RULES_SOURCE_NAME);
|
||||
const folderGroups = grafanaFolders.find((f) => f.uid === folderUid)?.groups ?? [];
|
||||
const folderGroups = Object.values(rulerNamespace).flat() ?? [];
|
||||
|
||||
const groupOptions = folderGroups
|
||||
.map<SelectableValue<string>>((group) => {
|
||||
const isProvisioned = isProvisionedGroup(group);
|
||||
return {
|
||||
label: group.name,
|
||||
value: group.name,
|
||||
description: group.interval ?? DEFAULT_GROUP_EVALUATION_INTERVAL,
|
||||
// we include provisioned folders, but disable the option to select them
|
||||
isDisabled: !enableProvisionedGroups ? isProvisioned : false,
|
||||
isProvisioned: isProvisioned,
|
||||
};
|
||||
})
|
||||
return folderGroups
|
||||
.map<SelectableValue<string>>((group) => {
|
||||
const isProvisioned = isProvisionedGroup(group);
|
||||
return {
|
||||
label: group.name,
|
||||
value: group.name,
|
||||
description: group.interval ?? DEFAULT_GROUP_EVALUATION_INTERVAL,
|
||||
// we include provisioned folders, but disable the option to select them
|
||||
isDisabled: !enableProvisionedGroups ? isProvisioned : false,
|
||||
isProvisioned: isProvisioned,
|
||||
};
|
||||
})
|
||||
|
||||
.sort(sortByLabel);
|
||||
.sort(sortByLabel);
|
||||
}, [rulerNamespace, enableProvisionedGroups]);
|
||||
|
||||
return { groupOptions, loading: groupfoldersForGrafana?.loading };
|
||||
return { groupOptions, loading: isLoadingRulerNamespace };
|
||||
};
|
||||
|
||||
const isProvisionedGroup = (group: CombinedRuleGroup) => {
|
||||
return group.rules.some(
|
||||
(rule) => isGrafanaRulerRule(rule.rulerRule) && Boolean(rule.rulerRule.grafana_alert.provenance) === true
|
||||
);
|
||||
const isProvisionedGroup = (group: RulerRuleGroupDTO) => {
|
||||
return group.rules.some((rule) => isGrafanaRulerRule(rule) && Boolean(rule.grafana_alert.provenance) === true);
|
||||
};
|
||||
|
||||
const sortByLabel = (a: SelectableValue<string>, b: SelectableValue<string>) => {
|
||||
|
@ -7,50 +7,39 @@ import { ui } from 'test/helpers/alertingRuleEditor';
|
||||
import { clickSelectOption } from 'test/helpers/selectOptionInTest';
|
||||
import { byRole } from 'testing-library-selector';
|
||||
|
||||
import { config, locationService, setDataSourceSrv } from '@grafana/runtime';
|
||||
import { config, locationService } from '@grafana/runtime';
|
||||
import { contextSrv } from 'app/core/services/context_srv';
|
||||
import RuleEditor from 'app/features/alerting/unified/RuleEditor';
|
||||
import { discoverFeatures } from 'app/features/alerting/unified/api/buildInfo';
|
||||
import {
|
||||
fetchRulerRules,
|
||||
fetchRulerRulesGroup,
|
||||
fetchRulerRulesNamespace,
|
||||
setRulerRuleGroup,
|
||||
} from 'app/features/alerting/unified/api/ruler';
|
||||
import * as ruler from 'app/features/alerting/unified/api/ruler';
|
||||
import * as useContactPoints from 'app/features/alerting/unified/components/contact-points/useContactPoints';
|
||||
import * as dsByPermission from 'app/features/alerting/unified/hooks/useAlertManagerSources';
|
||||
import { setupMswServer } from 'app/features/alerting/unified/mockApi';
|
||||
import { MockDataSourceSrv, grantUserPermissions, mockDataSource } from 'app/features/alerting/unified/mocks';
|
||||
import { grantUserPermissions, mockDataSource } from 'app/features/alerting/unified/mocks';
|
||||
import { AlertmanagerProvider } from 'app/features/alerting/unified/state/AlertmanagerContext';
|
||||
import { fetchRulerRulesIfNotFetchedYet } from 'app/features/alerting/unified/state/actions';
|
||||
import * as utils_config from 'app/features/alerting/unified/utils/config';
|
||||
import {
|
||||
AlertManagerDataSource,
|
||||
DataSourceType,
|
||||
GRAFANA_DATASOURCE_NAME,
|
||||
GRAFANA_RULES_SOURCE_NAME,
|
||||
getAlertManagerDataSourcesByPermission,
|
||||
useGetAlertManagerDataSourcesByPermissionAndConfig,
|
||||
} from 'app/features/alerting/unified/utils/datasource';
|
||||
import { getDefaultQueries } from 'app/features/alerting/unified/utils/rule-form';
|
||||
import { searchFolders } from 'app/features/manage-dashboards/state/actions';
|
||||
import { DashboardSearchHit, DashboardSearchItemType } from 'app/features/search/types';
|
||||
import { AccessControlAction } from 'app/types';
|
||||
import { GrafanaAlertStateDecision, PromApplication } from 'app/types/unified-alerting-dto';
|
||||
import { GrafanaAlertStateDecision } from 'app/types/unified-alerting-dto';
|
||||
|
||||
import { grafanaRulerEmptyGroup, grafanaRulerNamespace2, grafanaRulerRule } from '../../../../mocks/alertRuleApi';
|
||||
import { setupDataSources } from '../../../../testSetup/datasources';
|
||||
import { RECEIVER_META_KEY } from '../../../contact-points/useContactPoints';
|
||||
import { ContactPointWithMetadata } from '../../../contact-points/utils';
|
||||
import { ExpressionEditorProps } from '../../ExpressionEditor';
|
||||
|
||||
jest.mock('app/features/alerting/unified/components/rule-editor/ExpressionEditor', () => ({
|
||||
// eslint-disable-next-line react/display-name
|
||||
ExpressionEditor: ({ value, onChange }: ExpressionEditorProps) => (
|
||||
<input value={value} data-testid="expr" onChange={(e) => onChange(e.target.value)} />
|
||||
),
|
||||
}));
|
||||
|
||||
jest.mock('app/features/alerting/unified/api/buildInfo');
|
||||
jest.mock('app/features/alerting/unified/api/ruler');
|
||||
jest.mock('app/features/manage-dashboards/state/actions');
|
||||
|
||||
jest.mock('app/core/components/AppChrome/AppChromeUpdate', () => ({
|
||||
@ -60,29 +49,13 @@ jest.mock('app/core/components/AppChrome/AppChromeUpdate', () => ({
|
||||
// there's no angular scope in test and things go terribly wrong when trying to render the query editor row.
|
||||
// lets just skip it
|
||||
jest.mock('app/features/query/components/QueryEditorRow', () => ({
|
||||
// eslint-disable-next-line react/display-name
|
||||
QueryEditorRow: () => <p>hi</p>,
|
||||
}));
|
||||
|
||||
// simplified routing mocks
|
||||
const grafanaAlertManagerDataSource: AlertManagerDataSource = {
|
||||
name: GRAFANA_RULES_SOURCE_NAME,
|
||||
imgUrl: 'public/img/grafana_icon.svg',
|
||||
hasConfigurationAPI: true,
|
||||
};
|
||||
jest.mock('app/features/alerting/unified/utils/datasource', () => {
|
||||
return {
|
||||
...jest.requireActual('app/features/alerting/unified/utils/datasource'),
|
||||
getAlertManagerDataSourcesByPermission: jest.fn(),
|
||||
useGetAlertManagerDataSourcesByPermissionAndConfig: jest.fn(),
|
||||
getAlertmanagerDataSourceByName: jest.fn(),
|
||||
};
|
||||
});
|
||||
|
||||
const user = userEvent.setup();
|
||||
|
||||
jest.spyOn(utils_config, 'getAllDataSources');
|
||||
jest.spyOn(dsByPermission, 'useAlertManagersByPermission');
|
||||
// jest.spyOn(utils_config, 'getAllDataSources');
|
||||
// jest.spyOn(dsByPermission, 'useAlertManagersByPermission');
|
||||
jest.spyOn(useContactPoints, 'useContactPointsWithStatus');
|
||||
|
||||
jest.setTimeout(60 * 1000);
|
||||
@ -92,14 +65,8 @@ const mocks = {
|
||||
searchFolders: jest.mocked(searchFolders),
|
||||
useContactPointsWithStatus: jest.mocked(useContactPoints.useContactPointsWithStatus),
|
||||
useGetAlertManagerDataSourcesByPermissionAndConfig: jest.mocked(useGetAlertManagerDataSourcesByPermissionAndConfig),
|
||||
getAlertManagerDataSourcesByPermission: jest.mocked(getAlertManagerDataSourcesByPermission),
|
||||
api: {
|
||||
discoverFeatures: jest.mocked(discoverFeatures),
|
||||
fetchRulerRulesGroup: jest.mocked(fetchRulerRulesGroup),
|
||||
setRulerRuleGroup: jest.mocked(setRulerRuleGroup),
|
||||
fetchRulerRulesNamespace: jest.mocked(fetchRulerRulesNamespace),
|
||||
fetchRulerRules: jest.mocked(fetchRulerRules),
|
||||
fetchRulerRulesIfNotFetchedYet: jest.mocked(fetchRulerRulesIfNotFetchedYet),
|
||||
setRulerRuleGroup: jest.spyOn(ruler, 'setRulerRuleGroup'),
|
||||
},
|
||||
};
|
||||
|
||||
@ -125,17 +92,6 @@ describe('Can create a new grafana managed alert unsing simplified routing', ()
|
||||
AccessControlAction.AlertingNotificationsRead,
|
||||
AccessControlAction.AlertingNotificationsWrite,
|
||||
]);
|
||||
mocks.getAlertManagerDataSourcesByPermission.mockReturnValue({
|
||||
availableInternalDataSources: [grafanaAlertManagerDataSource],
|
||||
availableExternalDataSources: [],
|
||||
});
|
||||
|
||||
mocks.useGetAlertManagerDataSourcesByPermissionAndConfig.mockReturnValue([grafanaAlertManagerDataSource]);
|
||||
|
||||
jest.mocked(dsByPermission.useAlertManagersByPermission).mockReturnValue({
|
||||
availableInternalDataSources: [grafanaAlertManagerDataSource],
|
||||
availableExternalDataSources: [],
|
||||
});
|
||||
});
|
||||
|
||||
const dataSources = {
|
||||
@ -152,6 +108,7 @@ describe('Can create a new grafana managed alert unsing simplified routing', ()
|
||||
type: DataSourceType.Alertmanager,
|
||||
}),
|
||||
};
|
||||
setupDataSources(dataSources.default, dataSources.am);
|
||||
|
||||
it('cannot create new grafana managed alert when using simplified routing and not selecting a contact point', async () => {
|
||||
// no contact points found
|
||||
@ -162,64 +119,11 @@ describe('Can create a new grafana managed alert unsing simplified routing', ()
|
||||
refetchReceivers: jest.fn(),
|
||||
});
|
||||
|
||||
setDataSourceSrv(new MockDataSourceSrv(dataSources));
|
||||
mocks.getAllDataSources.mockReturnValue(Object.values(dataSources));
|
||||
mocks.api.setRulerRuleGroup.mockResolvedValue();
|
||||
mocks.api.fetchRulerRulesNamespace.mockResolvedValue([]);
|
||||
mocks.api.fetchRulerRulesGroup.mockResolvedValue({
|
||||
name: 'group2',
|
||||
rules: [],
|
||||
});
|
||||
mocks.api.fetchRulerRules.mockResolvedValue({
|
||||
'Folder A': [
|
||||
{
|
||||
interval: '1m',
|
||||
name: 'group1',
|
||||
rules: [
|
||||
{
|
||||
annotations: { description: 'some description', summary: 'some summary' },
|
||||
labels: { severity: 'warn', team: 'the a-team' },
|
||||
for: '1m',
|
||||
grafana_alert: {
|
||||
uid: '23',
|
||||
namespace_uid: 'abcd',
|
||||
condition: 'B',
|
||||
data: getDefaultQueries(),
|
||||
exec_err_state: GrafanaAlertStateDecision.Error,
|
||||
no_data_state: GrafanaAlertStateDecision.NoData,
|
||||
title: 'my great new rule',
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
namespace2: [
|
||||
{
|
||||
interval: '1m',
|
||||
name: 'group1',
|
||||
rules: [
|
||||
{
|
||||
annotations: { description: 'some description', summary: 'some summary' },
|
||||
labels: { severity: 'warn', team: 'the a-team' },
|
||||
for: '1m',
|
||||
grafana_alert: {
|
||||
uid: '23',
|
||||
namespace_uid: 'b',
|
||||
condition: 'B',
|
||||
data: getDefaultQueries(),
|
||||
exec_err_state: GrafanaAlertStateDecision.Error,
|
||||
no_data_state: GrafanaAlertStateDecision.NoData,
|
||||
title: 'my great new rule',
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
});
|
||||
mocks.searchFolders.mockResolvedValue([
|
||||
{
|
||||
title: 'Folder A',
|
||||
uid: 'abcd',
|
||||
uid: grafanaRulerRule.grafana_alert.namespace_uid,
|
||||
id: 1,
|
||||
type: DashboardSearchItemType.DashDB,
|
||||
},
|
||||
@ -235,12 +139,6 @@ describe('Can create a new grafana managed alert unsing simplified routing', ()
|
||||
},
|
||||
] as DashboardSearchHit[]);
|
||||
|
||||
mocks.api.discoverFeatures.mockResolvedValue({
|
||||
application: PromApplication.Prometheus,
|
||||
features: {
|
||||
rulerApiEnabled: false,
|
||||
},
|
||||
});
|
||||
config.featureToggles.alertingSimplifiedRouting = true;
|
||||
renderSimplifiedRuleEditor();
|
||||
await waitForElementToBeRemoved(screen.getAllByTestId('Spinner'));
|
||||
@ -251,7 +149,7 @@ describe('Can create a new grafana managed alert unsing simplified routing', ()
|
||||
await clickSelectOption(folderInput, 'Folder A');
|
||||
const groupInput = await ui.inputs.group.find();
|
||||
await user.click(byRole('combobox').get(groupInput));
|
||||
await clickSelectOption(groupInput, 'group1');
|
||||
await clickSelectOption(groupInput, grafanaRulerRule.grafana_alert.rule_group);
|
||||
//select contact point routing
|
||||
await user.click(ui.inputs.simplifiedRouting.contactPointRouting.get());
|
||||
// do not select a contact point
|
||||
@ -289,64 +187,11 @@ describe('Can create a new grafana managed alert unsing simplified routing', ()
|
||||
refetchReceivers: jest.fn(),
|
||||
});
|
||||
|
||||
setDataSourceSrv(new MockDataSourceSrv(dataSources));
|
||||
mocks.getAllDataSources.mockReturnValue(Object.values(dataSources));
|
||||
mocks.api.setRulerRuleGroup.mockResolvedValue();
|
||||
mocks.api.fetchRulerRulesNamespace.mockResolvedValue([]);
|
||||
mocks.api.fetchRulerRulesGroup.mockResolvedValue({
|
||||
name: 'group2',
|
||||
rules: [],
|
||||
});
|
||||
mocks.api.fetchRulerRules.mockResolvedValue({
|
||||
'Folder A': [
|
||||
{
|
||||
interval: '1m',
|
||||
name: 'group1',
|
||||
rules: [
|
||||
{
|
||||
annotations: { description: 'some description', summary: 'some summary' },
|
||||
labels: { severity: 'warn', team: 'the a-team' },
|
||||
for: '1m',
|
||||
grafana_alert: {
|
||||
uid: '23',
|
||||
namespace_uid: 'abcd',
|
||||
condition: 'B',
|
||||
data: getDefaultQueries(),
|
||||
exec_err_state: GrafanaAlertStateDecision.Error,
|
||||
no_data_state: GrafanaAlertStateDecision.NoData,
|
||||
title: 'my great new rule',
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
namespace2: [
|
||||
{
|
||||
interval: '1m',
|
||||
name: 'group1',
|
||||
rules: [
|
||||
{
|
||||
annotations: { description: 'some description', summary: 'some summary' },
|
||||
labels: { severity: 'warn', team: 'the a-team' },
|
||||
for: '1m',
|
||||
grafana_alert: {
|
||||
uid: '23',
|
||||
namespace_uid: 'b',
|
||||
condition: 'B',
|
||||
data: getDefaultQueries(),
|
||||
exec_err_state: GrafanaAlertStateDecision.Error,
|
||||
no_data_state: GrafanaAlertStateDecision.NoData,
|
||||
title: 'my great new rule',
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
});
|
||||
mocks.searchFolders.mockResolvedValue([
|
||||
{
|
||||
title: 'Folder A',
|
||||
uid: 'abcd',
|
||||
uid: grafanaRulerNamespace2.uid,
|
||||
id: 1,
|
||||
type: DashboardSearchItemType.DashDB,
|
||||
},
|
||||
@ -364,12 +209,6 @@ describe('Can create a new grafana managed alert unsing simplified routing', ()
|
||||
},
|
||||
] as DashboardSearchHit[]);
|
||||
|
||||
mocks.api.discoverFeatures.mockResolvedValue({
|
||||
application: PromApplication.Prometheus,
|
||||
features: {
|
||||
rulerApiEnabled: false,
|
||||
},
|
||||
});
|
||||
config.featureToggles.alertingSimplifiedRouting = true;
|
||||
renderSimplifiedRuleEditor();
|
||||
await waitForElementToBeRemoved(screen.getAllByTestId('Spinner'));
|
||||
@ -380,7 +219,7 @@ describe('Can create a new grafana managed alert unsing simplified routing', ()
|
||||
await clickSelectOption(folderInput, 'Folder A');
|
||||
const groupInput = await ui.inputs.group.find();
|
||||
await user.click(byRole('combobox').get(groupInput));
|
||||
await clickSelectOption(groupInput, 'group1');
|
||||
await clickSelectOption(groupInput, grafanaRulerEmptyGroup.name);
|
||||
//select contact point routing
|
||||
await user.click(ui.inputs.simplifiedRouting.contactPointRouting.get());
|
||||
const contactPointInput = await ui.inputs.simplifiedRouting.contactPoint.find();
|
||||
@ -392,10 +231,10 @@ describe('Can create a new grafana managed alert unsing simplified routing', ()
|
||||
await waitFor(() => expect(mocks.api.setRulerRuleGroup).toHaveBeenCalled());
|
||||
expect(mocks.api.setRulerRuleGroup).toHaveBeenCalledWith(
|
||||
{ dataSourceName: GRAFANA_RULES_SOURCE_NAME, apiVersion: 'legacy' },
|
||||
'abcd',
|
||||
grafanaRulerNamespace2.uid,
|
||||
{
|
||||
interval: '1m',
|
||||
name: 'group1',
|
||||
interval: grafanaRulerEmptyGroup.interval,
|
||||
name: grafanaRulerEmptyGroup.name,
|
||||
rules: [
|
||||
{
|
||||
annotations: {},
|
||||
|
@ -1,24 +1,24 @@
|
||||
import { within } from '@testing-library/react';
|
||||
import React from 'react';
|
||||
import { render, waitFor, screen, userEvent } from 'test/test-utils';
|
||||
import { byText, byRole } from 'testing-library-selector';
|
||||
|
||||
import { setBackendSrv, setDataSourceSrv, setPluginExtensionsHook } from '@grafana/runtime';
|
||||
import { setBackendSrv, setPluginExtensionsHook } from '@grafana/runtime';
|
||||
import { backendSrv } from 'app/core/services/backend_srv';
|
||||
import { setupMswServer } from 'app/features/alerting/unified/mockApi';
|
||||
import { setFolderAccessControl } from 'app/features/alerting/unified/mocks/server/configure';
|
||||
import { MOCK_GRAFANA_ALERT_RULE_TITLE } from 'app/features/alerting/unified/mocks/server/handlers/alertRules';
|
||||
import { AlertManagerDataSourceJsonData } from 'app/plugins/datasource/alertmanager/types';
|
||||
import { AccessControlAction } from 'app/types';
|
||||
import { CombinedRule, RuleIdentifier } from 'app/types/unified-alerting';
|
||||
|
||||
import {
|
||||
MockDataSourceSrv,
|
||||
getCloudRule,
|
||||
getGrafanaRule,
|
||||
grantUserPermissions,
|
||||
mockDataSource,
|
||||
mockPluginLinkExtension,
|
||||
} from '../../mocks';
|
||||
import { grafanaRulerRule } from '../../mocks/alertRuleApi';
|
||||
import { setupDataSources } from '../../testSetup/datasources';
|
||||
import { Annotation } from '../../utils/constants';
|
||||
import { DataSourceType, GRAFANA_RULES_SOURCE_NAME } from '../../utils/datasource';
|
||||
@ -104,7 +104,7 @@ describe('RuleViewer', () => {
|
||||
totals: { alerting: 1 },
|
||||
},
|
||||
},
|
||||
{ uid: 'test1' }
|
||||
{ uid: grafanaRulerRule.grafana_alert.uid }
|
||||
);
|
||||
const mockRuleIdentifier = ruleId.fromCombinedRule('grafana', mockRule);
|
||||
|
||||
@ -114,6 +114,7 @@ describe('RuleViewer', () => {
|
||||
AccessControlAction.AlertingRuleRead,
|
||||
AccessControlAction.AlertingRuleUpdate,
|
||||
AccessControlAction.AlertingRuleDelete,
|
||||
AccessControlAction.AlertingInstanceRead,
|
||||
AccessControlAction.AlertingInstanceCreate,
|
||||
]);
|
||||
setBackendSrv(backendSrv);
|
||||
@ -181,7 +182,6 @@ describe('RuleViewer', () => {
|
||||
}),
|
||||
};
|
||||
setupDataSources(dataSources.grafana, dataSources.am);
|
||||
setDataSourceSrv(new MockDataSourceSrv(dataSources));
|
||||
|
||||
await renderRuleViewer(mockRule, mockRuleIdentifier);
|
||||
|
||||
@ -189,7 +189,10 @@ describe('RuleViewer', () => {
|
||||
await user.click(ELEMENTS.actions.more.button.get());
|
||||
await user.click(ELEMENTS.actions.more.actions.silence.get());
|
||||
|
||||
expect(await screen.findByLabelText(/^alert rule/i)).toHaveValue(MOCK_GRAFANA_ALERT_RULE_TITLE);
|
||||
const silenceDrawer = await screen.findByRole('dialog', { name: 'Drawer title Silence alert rule' });
|
||||
expect(await within(silenceDrawer).findByLabelText(/^alert rule/i)).toHaveValue(
|
||||
grafanaRulerRule.grafana_alert.title
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
|
@ -2,7 +2,14 @@ import { useEffect, useMemo } from 'react';
|
||||
import { useAsync } from 'react-use';
|
||||
|
||||
import { useDispatch } from 'app/types';
|
||||
import { CombinedRule, RuleIdentifier, RuleNamespace, RulerDataSourceConfig } from 'app/types/unified-alerting';
|
||||
import {
|
||||
CombinedRule,
|
||||
RuleIdentifier,
|
||||
RuleNamespace,
|
||||
RulerDataSourceConfig,
|
||||
RulesSource,
|
||||
RuleWithLocation,
|
||||
} from 'app/types/unified-alerting';
|
||||
import { RulerRuleGroupDTO, RulerRulesConfigDTO } from 'app/types/unified-alerting-dto';
|
||||
|
||||
import { alertRuleApi } from '../api/alertRuleApi';
|
||||
@ -18,11 +25,7 @@ import {
|
||||
isRulerNotSupportedResponse,
|
||||
} from '../utils/rules';
|
||||
|
||||
import {
|
||||
attachRulerRulesToCombinedRules,
|
||||
combineRulesNamespaces,
|
||||
useCombinedRuleNamespaces,
|
||||
} from './useCombinedRuleNamespaces';
|
||||
import { attachRulerRulesToCombinedRules, useCombinedRuleNamespaces } from './useCombinedRuleNamespaces';
|
||||
import { useUnifiedAlertingSelector } from './useUnifiedAlertingSelector';
|
||||
|
||||
export function useCombinedRulesMatching(
|
||||
@ -162,15 +165,25 @@ function getRequestState(
|
||||
return state;
|
||||
}
|
||||
|
||||
export function useCombinedRule({ ruleIdentifier }: { ruleIdentifier: RuleIdentifier }): {
|
||||
interface RequestState<T> {
|
||||
result?: T;
|
||||
loading: boolean;
|
||||
result?: CombinedRule;
|
||||
error?: unknown;
|
||||
} {
|
||||
}
|
||||
|
||||
// Many places still use the old way of fetching code so synchronizing cache expiration is difficult
|
||||
// Hence, this hook fetches a fresh version of a rule most of the time
|
||||
// Due to enabled filtering for Prometheus and Ruler rules it shouldn't be a problem
|
||||
export function useCombinedRule({ ruleIdentifier }: { ruleIdentifier: RuleIdentifier }): RequestState<CombinedRule> {
|
||||
const { ruleSourceName } = ruleIdentifier;
|
||||
const dsSettings = getDataSourceByName(ruleSourceName);
|
||||
const ruleSource = getRulesSourceFromIdentifier(ruleIdentifier);
|
||||
|
||||
const { dsFeatures, isLoadingDsFeatures } = useDataSourceFeatures(ruleSourceName);
|
||||
const {
|
||||
loading: isLoadingRuleLocation,
|
||||
error: ruleLocationError,
|
||||
result: ruleLocation,
|
||||
} = useRuleLocation(ruleIdentifier);
|
||||
|
||||
const {
|
||||
currentData: promRuleNs,
|
||||
@ -178,85 +191,47 @@ export function useCombinedRule({ ruleIdentifier }: { ruleIdentifier: RuleIdenti
|
||||
error: promRuleNsError,
|
||||
} = alertRuleApi.endpoints.prometheusRuleNamespaces.useQuery(
|
||||
{
|
||||
// TODO Refactor parameters
|
||||
ruleSourceName: ruleIdentifier.ruleSourceName,
|
||||
namespace:
|
||||
isPrometheusRuleIdentifier(ruleIdentifier) || isCloudRuleIdentifier(ruleIdentifier)
|
||||
? ruleIdentifier.namespace
|
||||
: undefined,
|
||||
groupName:
|
||||
isPrometheusRuleIdentifier(ruleIdentifier) || isCloudRuleIdentifier(ruleIdentifier)
|
||||
? ruleIdentifier.groupName
|
||||
: undefined,
|
||||
ruleName:
|
||||
isPrometheusRuleIdentifier(ruleIdentifier) || isCloudRuleIdentifier(ruleIdentifier)
|
||||
? ruleIdentifier.ruleName
|
||||
: undefined,
|
||||
namespace: ruleLocation?.namespace,
|
||||
groupName: ruleLocation?.group,
|
||||
ruleName: ruleLocation?.ruleName,
|
||||
},
|
||||
{
|
||||
skip: !ruleLocation || isLoadingRuleLocation,
|
||||
refetchOnMountOrArgChange: true,
|
||||
}
|
||||
// TODO – experiment with enabling these now that we request a single alert rule more efficiently.
|
||||
// Requires a recent version of Prometheus with support for query params on /api/v1/rules
|
||||
// {
|
||||
// refetchOnFocus: true,
|
||||
// refetchOnReconnect: true,
|
||||
// }
|
||||
);
|
||||
|
||||
const [
|
||||
fetchRulerRuleGroup,
|
||||
{ currentData: rulerRuleGroup, isLoading: isLoadingRulerGroup, error: rulerRuleGroupError },
|
||||
{
|
||||
currentData: rulerRuleGroup,
|
||||
isLoading: isLoadingRulerGroup,
|
||||
error: rulerRuleGroupError,
|
||||
isUninitialized: rulerRuleGroupUninitialized,
|
||||
},
|
||||
] = alertRuleApi.endpoints.rulerRuleGroup.useLazyQuery();
|
||||
|
||||
const [fetchRulerRules, { currentData: rulerRules, isLoading: isLoadingRulerRules, error: rulerRulesError }] =
|
||||
alertRuleApi.endpoints.rulerRules.useLazyQuery();
|
||||
|
||||
useEffect(() => {
|
||||
if (!dsFeatures?.rulerConfig) {
|
||||
if (!dsFeatures?.rulerConfig || !ruleLocation) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (dsFeatures.rulerConfig && isCloudRuleIdentifier(ruleIdentifier)) {
|
||||
fetchRulerRuleGroup({
|
||||
rulerConfig: dsFeatures.rulerConfig,
|
||||
namespace: ruleIdentifier.namespace,
|
||||
group: ruleIdentifier.groupName,
|
||||
});
|
||||
} else if (isGrafanaRuleIdentifier(ruleIdentifier)) {
|
||||
// TODO Fetch a single group for Grafana managed rules, we're currently still fetching all rules for Grafana managed
|
||||
fetchRulerRules({ rulerConfig: dsFeatures.rulerConfig });
|
||||
}
|
||||
}, [dsFeatures, fetchRulerRuleGroup, fetchRulerRules, ruleIdentifier]);
|
||||
fetchRulerRuleGroup({
|
||||
rulerConfig: dsFeatures.rulerConfig,
|
||||
namespace: ruleLocation.namespace,
|
||||
group: ruleLocation.group,
|
||||
});
|
||||
}, [dsFeatures, fetchRulerRuleGroup, ruleLocation]);
|
||||
|
||||
const rule = useMemo(() => {
|
||||
if (!promRuleNs) {
|
||||
if (!promRuleNs || !ruleSource) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (isGrafanaRuleIdentifier(ruleIdentifier)) {
|
||||
const combinedNamespaces = combineRulesNamespaces('grafana', promRuleNs, rulerRules);
|
||||
|
||||
for (const namespace of combinedNamespaces) {
|
||||
for (const group of namespace.groups) {
|
||||
for (const rule of group.rules) {
|
||||
const id = ruleId.fromCombinedRule(ruleSourceName, rule);
|
||||
|
||||
if (ruleId.equal(id, ruleIdentifier)) {
|
||||
return rule;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!dsSettings) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (
|
||||
promRuleNs.length > 0 &&
|
||||
(isCloudRuleIdentifier(ruleIdentifier) || isPrometheusRuleIdentifier(ruleIdentifier))
|
||||
) {
|
||||
if (promRuleNs.length > 0) {
|
||||
const namespaces = promRuleNs.map((ns) =>
|
||||
attachRulerRulesToCombinedRules(dsSettings, ns, rulerRuleGroup ? [rulerRuleGroup] : [])
|
||||
attachRulerRulesToCombinedRules(ruleSource, ns, rulerRuleGroup ? [rulerRuleGroup] : [])
|
||||
);
|
||||
|
||||
for (const namespace of namespaces) {
|
||||
@ -273,15 +248,147 @@ export function useCombinedRule({ ruleIdentifier }: { ruleIdentifier: RuleIdenti
|
||||
}
|
||||
|
||||
return;
|
||||
}, [ruleIdentifier, ruleSourceName, promRuleNs, rulerRuleGroup, rulerRules, dsSettings]);
|
||||
}, [ruleIdentifier, ruleSourceName, promRuleNs, rulerRuleGroup, ruleSource]);
|
||||
|
||||
return {
|
||||
loading: isLoadingDsFeatures || isLoadingPromRules || isLoadingRulerGroup || isLoadingRulerRules,
|
||||
error: promRuleNsError ?? rulerRuleGroupError ?? rulerRulesError,
|
||||
loading: isLoadingDsFeatures || isLoadingPromRules || isLoadingRulerGroup || rulerRuleGroupUninitialized,
|
||||
error: ruleLocationError ?? promRuleNsError ?? rulerRuleGroupError,
|
||||
result: rule,
|
||||
};
|
||||
}
|
||||
|
||||
interface RuleLocation {
|
||||
namespace: string;
|
||||
group: string;
|
||||
ruleName: string;
|
||||
}
|
||||
|
||||
function useRuleLocation(ruleIdentifier: RuleIdentifier): RequestState<RuleLocation> {
|
||||
const { isLoading, currentData, error, isUninitialized } = alertRuleApi.endpoints.getAlertRule.useQuery(
|
||||
{ uid: isGrafanaRuleIdentifier(ruleIdentifier) ? ruleIdentifier.uid : '' },
|
||||
{ skip: !isGrafanaRuleIdentifier(ruleIdentifier), refetchOnMountOrArgChange: true }
|
||||
);
|
||||
|
||||
return useMemo(() => {
|
||||
if (isPrometheusRuleIdentifier(ruleIdentifier) || isCloudRuleIdentifier(ruleIdentifier)) {
|
||||
return {
|
||||
result: {
|
||||
namespace: ruleIdentifier.namespace,
|
||||
group: ruleIdentifier.groupName,
|
||||
ruleName: ruleIdentifier.ruleName,
|
||||
},
|
||||
loading: false,
|
||||
};
|
||||
}
|
||||
|
||||
if (isGrafanaRuleIdentifier(ruleIdentifier)) {
|
||||
if (isLoading || isUninitialized) {
|
||||
return { loading: true };
|
||||
}
|
||||
|
||||
if (error) {
|
||||
return { loading: false, error };
|
||||
}
|
||||
if (currentData) {
|
||||
return {
|
||||
result: {
|
||||
namespace: currentData.grafana_alert.namespace_uid,
|
||||
group: currentData.grafana_alert.rule_group,
|
||||
ruleName: currentData.grafana_alert.title,
|
||||
},
|
||||
loading: false,
|
||||
};
|
||||
}
|
||||
|
||||
// In theory, this should never happen
|
||||
return {
|
||||
loading: false,
|
||||
error: new Error(`Unable to obtain rule location for rule ${ruleIdentifier.uid}`),
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
loading: false,
|
||||
error: new Error('Unsupported rule identifier'),
|
||||
};
|
||||
}, [ruleIdentifier, isLoading, isUninitialized, error, currentData]);
|
||||
}
|
||||
|
||||
function getRulesSourceFromIdentifier(ruleIdentifier: RuleIdentifier): RulesSource | undefined {
|
||||
if (isGrafanaRuleIdentifier(ruleIdentifier)) {
|
||||
return 'grafana';
|
||||
}
|
||||
|
||||
return getDataSourceByName(ruleIdentifier.ruleSourceName);
|
||||
}
|
||||
|
||||
// This Hook fetches rule definition from the Ruler API only
|
||||
export function useRuleWithLocation({
|
||||
ruleIdentifier,
|
||||
}: {
|
||||
ruleIdentifier: RuleIdentifier;
|
||||
}): RequestState<RuleWithLocation> {
|
||||
const ruleSource = getRulesSourceFromIdentifier(ruleIdentifier);
|
||||
|
||||
const { dsFeatures, isLoadingDsFeatures } = useDataSourceFeatures(ruleIdentifier.ruleSourceName);
|
||||
const {
|
||||
loading: isLoadingRuleLocation,
|
||||
error: ruleLocationError,
|
||||
result: ruleLocation,
|
||||
} = useRuleLocation(ruleIdentifier);
|
||||
|
||||
const [
|
||||
fetchRulerRuleGroup,
|
||||
{
|
||||
currentData: rulerRuleGroup,
|
||||
isLoading: isLoadingRulerGroup,
|
||||
isUninitialized: isUninitializedRulerGroup,
|
||||
error: rulerRuleGroupError,
|
||||
},
|
||||
] = alertRuleApi.endpoints.rulerRuleGroup.useLazyQuery();
|
||||
|
||||
useEffect(() => {
|
||||
if (!dsFeatures?.rulerConfig || !ruleLocation) {
|
||||
return;
|
||||
}
|
||||
|
||||
fetchRulerRuleGroup({
|
||||
rulerConfig: dsFeatures.rulerConfig,
|
||||
namespace: ruleLocation.namespace,
|
||||
group: ruleLocation.group,
|
||||
});
|
||||
}, [dsFeatures, fetchRulerRuleGroup, ruleLocation]);
|
||||
|
||||
const ruleWithLocation = useMemo(() => {
|
||||
const { ruleSourceName } = ruleIdentifier;
|
||||
if (!rulerRuleGroup || !ruleSource || !ruleLocation) {
|
||||
return;
|
||||
}
|
||||
|
||||
const rule = rulerRuleGroup.rules.find((rule) => {
|
||||
const id = ruleId.fromRulerRule(ruleSourceName, ruleLocation.namespace, ruleLocation.group, rule);
|
||||
return ruleId.equal(id, ruleIdentifier);
|
||||
});
|
||||
|
||||
if (!rule) {
|
||||
return;
|
||||
}
|
||||
|
||||
return {
|
||||
ruleSourceName: ruleSourceName,
|
||||
group: rulerRuleGroup,
|
||||
namespace: ruleLocation.namespace,
|
||||
rule: rule,
|
||||
};
|
||||
}, [ruleIdentifier, rulerRuleGroup, ruleSource, ruleLocation]);
|
||||
|
||||
return {
|
||||
loading: isLoadingRuleLocation || isLoadingDsFeatures || isLoadingRulerGroup || isUninitializedRulerGroup,
|
||||
error: ruleLocationError ?? rulerRuleGroupError,
|
||||
result: ruleWithLocation,
|
||||
};
|
||||
}
|
||||
|
||||
export const grafanaRulerConfig: RulerDataSourceConfig = {
|
||||
dataSourceName: GRAFANA_RULES_SOURCE_NAME,
|
||||
apiVersion: 'legacy',
|
||||
|
@ -117,6 +117,7 @@ export const mockRulerGrafanaRule = (
|
||||
uid: '123',
|
||||
title: 'myalert',
|
||||
namespace_uid: '123',
|
||||
rule_group: 'my-group',
|
||||
condition: 'A',
|
||||
no_data_state: GrafanaAlertStateDecision.Alerting,
|
||||
exec_err_state: GrafanaAlertStateDecision.Alerting,
|
||||
@ -214,6 +215,7 @@ export const mockGrafanaRulerRule = (partial: Partial<GrafanaRuleDefinition> = {
|
||||
uid: '',
|
||||
title: 'my rule',
|
||||
namespace_uid: 'NAMESPACE_UID',
|
||||
rule_group: 'my-group',
|
||||
condition: '',
|
||||
no_data_state: GrafanaAlertStateDecision.NoData,
|
||||
exec_err_state: GrafanaAlertStateDecision.Error,
|
||||
|
@ -1,9 +1,15 @@
|
||||
import { http, HttpResponse } from 'msw';
|
||||
import { SetupServer } from 'msw/node';
|
||||
|
||||
import { PromRulesResponse } from 'app/types/unified-alerting-dto';
|
||||
import {
|
||||
GrafanaAlertStateDecision,
|
||||
PromRulesResponse,
|
||||
RulerGrafanaRuleDTO,
|
||||
RulerRuleGroupDTO,
|
||||
} from 'app/types/unified-alerting-dto';
|
||||
|
||||
import { PreviewResponse, PREVIEW_URL, PROM_RULES_URL } from '../api/alertRuleApi';
|
||||
import { Annotation } from '../utils/constants';
|
||||
|
||||
export function mockPreviewApiResponse(server: SetupServer, result: PreviewResponse) {
|
||||
server.use(http.post(PREVIEW_URL, () => HttpResponse.json(result)));
|
||||
@ -12,3 +18,65 @@ export function mockPreviewApiResponse(server: SetupServer, result: PreviewRespo
|
||||
export function mockPromRulesApiResponse(server: SetupServer, result: PromRulesResponse) {
|
||||
server.use(http.get(PROM_RULES_URL, () => HttpResponse.json(result)));
|
||||
}
|
||||
|
||||
const grafanaRulerGroupName = 'grafana-group-1';
|
||||
export const grafanaRulerNamespace = { name: 'test-folder-1', uid: 'uuid020c61ef' };
|
||||
export const grafanaRulerNamespace2 = { name: 'test-folder-2', uid: '6abdb25bc1eb' };
|
||||
|
||||
export const grafanaRulerRule: RulerGrafanaRuleDTO = {
|
||||
for: '5m',
|
||||
labels: {
|
||||
severity: 'critical',
|
||||
region: 'nasa',
|
||||
},
|
||||
annotations: {
|
||||
[Annotation.summary]: 'Test alert',
|
||||
},
|
||||
grafana_alert: {
|
||||
uid: '4d7125fee983',
|
||||
title: 'Grafana-rule',
|
||||
namespace_uid: 'uuid020c61ef',
|
||||
rule_group: grafanaRulerGroupName,
|
||||
data: [
|
||||
{
|
||||
refId: 'A',
|
||||
datasourceUid: 'datasource-uid',
|
||||
queryType: 'alerting',
|
||||
relativeTimeRange: { from: 1000, to: 2000 },
|
||||
model: {
|
||||
refId: 'A',
|
||||
expression: 'vector(1)',
|
||||
queryType: 'alerting',
|
||||
datasource: { uid: 'datasource-uid', type: 'prometheus' },
|
||||
},
|
||||
},
|
||||
],
|
||||
condition: 'A',
|
||||
no_data_state: GrafanaAlertStateDecision.NoData,
|
||||
exec_err_state: GrafanaAlertStateDecision.Error,
|
||||
is_paused: false,
|
||||
notification_settings: undefined,
|
||||
},
|
||||
};
|
||||
|
||||
export const grafanaRulerGroup: RulerRuleGroupDTO = {
|
||||
name: grafanaRulerGroupName,
|
||||
interval: '1m',
|
||||
rules: [grafanaRulerRule],
|
||||
};
|
||||
|
||||
export const grafanaRulerEmptyGroup: RulerRuleGroupDTO = {
|
||||
name: 'empty-group',
|
||||
interval: '1m',
|
||||
rules: [],
|
||||
};
|
||||
|
||||
export const namespaceByUid: Record<string, { name: string; uid: string }> = {
|
||||
[grafanaRulerNamespace.uid]: grafanaRulerNamespace,
|
||||
[grafanaRulerNamespace2.uid]: grafanaRulerNamespace2,
|
||||
};
|
||||
|
||||
export const namespaces: Record<string, RulerRuleGroupDTO[]> = {
|
||||
[grafanaRulerNamespace.uid]: [grafanaRulerGroup],
|
||||
[grafanaRulerNamespace2.uid]: [grafanaRulerEmptyGroup],
|
||||
};
|
||||
|
@ -21,6 +21,7 @@ const allHandlers = [
|
||||
...folderHandlers,
|
||||
...pluginsHandlers,
|
||||
...silenceHandlers,
|
||||
...alertRuleHandlers,
|
||||
];
|
||||
|
||||
export default allHandlers;
|
||||
|
@ -0,0 +1,75 @@
|
||||
import { http, HttpResponse } from 'msw';
|
||||
|
||||
import {
|
||||
RulerGrafanaRuleDTO,
|
||||
RulerRuleGroupDTO,
|
||||
RulerRulesConfigDTO,
|
||||
} from '../../../../../../types/unified-alerting-dto';
|
||||
import { grafanaRulerRule, namespaceByUid, namespaces } from '../../alertRuleApi';
|
||||
|
||||
export const rulerRulesHandler = () => {
|
||||
return http.get(`/api/ruler/grafana/api/v1/rules`, () => {
|
||||
const response = Object.entries(namespaces).reduce<RulerRulesConfigDTO>((acc, [namespaceUid, groups]) => {
|
||||
acc[namespaceByUid[namespaceUid].name] = groups;
|
||||
return acc;
|
||||
}, {});
|
||||
|
||||
return HttpResponse.json<RulerRulesConfigDTO>(response);
|
||||
});
|
||||
};
|
||||
|
||||
export const rulerRuleNamespaceHandler = () => {
|
||||
return http.get<{ folderUid: string }>(`/api/ruler/grafana/api/v1/rules/:folderUid`, ({ params: { folderUid } }) => {
|
||||
// This mimic API response as closely as possible - Invalid folderUid returns 403
|
||||
const namespace = namespaces[folderUid];
|
||||
if (!namespace) {
|
||||
return new HttpResponse(null, { status: 403 });
|
||||
}
|
||||
|
||||
return HttpResponse.json<RulerRulesConfigDTO>({
|
||||
[namespaceByUid[folderUid].name]: namespaces[folderUid],
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
export const rulerRuleGroupHandler = () => {
|
||||
return http.get<{ folderUid: string; groupName: string }>(
|
||||
`/api/ruler/grafana/api/v1/rules/:folderUid/:groupName`,
|
||||
({ params: { folderUid, groupName } }) => {
|
||||
// This mimic API response as closely as possible.
|
||||
// Invalid folderUid returns 403 but invalid group will return 202 with empty list of rules
|
||||
const namespace = namespaces[folderUid];
|
||||
if (!namespace) {
|
||||
return new HttpResponse(null, { status: 403 });
|
||||
}
|
||||
|
||||
const matchingGroup = namespace.find((group) => group.name === groupName);
|
||||
return HttpResponse.json<RulerRuleGroupDTO>({
|
||||
name: groupName,
|
||||
interval: matchingGroup?.interval,
|
||||
rules: matchingGroup?.rules ?? [],
|
||||
});
|
||||
}
|
||||
);
|
||||
};
|
||||
|
||||
export const getAlertRuleHandler = () => {
|
||||
const grafanaRules = new Map<string, RulerGrafanaRuleDTO>(
|
||||
[grafanaRulerRule].map((rule) => [rule.grafana_alert.uid, rule])
|
||||
);
|
||||
|
||||
return http.get<{ uid: string }>(`/api/ruler/grafana/api/v1/rule/:uid`, ({ params: { uid } }) => {
|
||||
const rule = grafanaRules.get(uid);
|
||||
if (!rule) {
|
||||
return new HttpResponse(null, { status: 404 });
|
||||
}
|
||||
return HttpResponse.json(rule);
|
||||
});
|
||||
};
|
||||
|
||||
export const alertRuleHandlers = [
|
||||
rulerRulesHandler(),
|
||||
rulerRuleNamespaceHandler(),
|
||||
rulerRuleGroupHandler(),
|
||||
getAlertRuleHandler(),
|
||||
];
|
@ -2,15 +2,72 @@ import { http, HttpResponse } from 'msw';
|
||||
|
||||
export const MOCK_GRAFANA_ALERT_RULE_TITLE = 'Test alert';
|
||||
|
||||
const alertRuleDetailsHandler = () =>
|
||||
http.get<{ folderUid: string }>(`/api/ruler/:ruler/api/v1/rule/:uid`, () => {
|
||||
// TODO: Scaffold out alert rule response logic as this endpoint is used more in tests
|
||||
return HttpResponse.json({
|
||||
grafana_alert: {
|
||||
title: MOCK_GRAFANA_ALERT_RULE_TITLE,
|
||||
},
|
||||
import {
|
||||
RulerGrafanaRuleDTO,
|
||||
RulerRuleGroupDTO,
|
||||
RulerRulesConfigDTO,
|
||||
} from '../../../../../../types/unified-alerting-dto';
|
||||
import { grafanaRulerRule, namespaceByUid, namespaces } from '../../alertRuleApi';
|
||||
|
||||
export const rulerRulesHandler = () => {
|
||||
return http.get(`/api/ruler/grafana/api/v1/rules`, () => {
|
||||
const response = Object.entries(namespaces).reduce<RulerRulesConfigDTO>((acc, [namespaceUid, groups]) => {
|
||||
acc[namespaceByUid[namespaceUid].name] = groups;
|
||||
return acc;
|
||||
}, {});
|
||||
|
||||
return HttpResponse.json<RulerRulesConfigDTO>(response);
|
||||
});
|
||||
};
|
||||
|
||||
export const rulerRuleNamespaceHandler = () => {
|
||||
return http.get<{ folderUid: string }>(`/api/ruler/grafana/api/v1/rules/:folderUid`, ({ params: { folderUid } }) => {
|
||||
// This mimic API response as closely as possible - Invalid folderUid returns 403
|
||||
const namespace = namespaces[folderUid];
|
||||
if (!namespace) {
|
||||
return new HttpResponse(null, { status: 403 });
|
||||
}
|
||||
|
||||
return HttpResponse.json<RulerRulesConfigDTO>({
|
||||
[namespaceByUid[folderUid].name]: namespaces[folderUid],
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
const handlers = [alertRuleDetailsHandler()];
|
||||
export const rulerRuleGroupHandler = () => {
|
||||
return http.get<{ folderUid: string; groupName: string }>(
|
||||
`/api/ruler/grafana/api/v1/rules/:folderUid/:groupName`,
|
||||
({ params: { folderUid, groupName } }) => {
|
||||
// This mimic API response as closely as possible.
|
||||
// Invalid folderUid returns 403 but invalid group will return 202 with empty list of rules
|
||||
const namespace = namespaces[folderUid];
|
||||
if (!namespace) {
|
||||
return new HttpResponse(null, { status: 403 });
|
||||
}
|
||||
|
||||
const matchingGroup = namespace.find((group) => group.name === groupName);
|
||||
return HttpResponse.json<RulerRuleGroupDTO>({
|
||||
name: groupName,
|
||||
interval: matchingGroup?.interval,
|
||||
rules: matchingGroup?.rules ?? [],
|
||||
});
|
||||
}
|
||||
);
|
||||
};
|
||||
|
||||
export const rulerRuleHandler = () => {
|
||||
const grafanaRules = new Map<string, RulerGrafanaRuleDTO>(
|
||||
[grafanaRulerRule].map((rule) => [rule.grafana_alert.uid, rule])
|
||||
);
|
||||
|
||||
return http.get<{ uid: string }>(`/api/ruler/grafana/api/v1/rule/:uid`, ({ params: { uid } }) => {
|
||||
const rule = grafanaRules.get(uid);
|
||||
if (!rule) {
|
||||
return new HttpResponse(null, { status: 404 });
|
||||
}
|
||||
return HttpResponse.json(rule);
|
||||
});
|
||||
};
|
||||
|
||||
const handlers = [rulerRulesHandler(), rulerRuleNamespaceHandler(), rulerRuleGroupHandler(), rulerRuleHandler()];
|
||||
export default handlers;
|
||||
|
@ -17,8 +17,8 @@ import {
|
||||
PromBasedDataSource,
|
||||
RuleIdentifier,
|
||||
RuleNamespace,
|
||||
RulerDataSourceConfig,
|
||||
RuleWithLocation,
|
||||
RulerDataSourceConfig,
|
||||
StateHistoryItem,
|
||||
} from 'app/types/unified-alerting';
|
||||
import {
|
||||
@ -30,15 +30,16 @@ import {
|
||||
|
||||
import { backendSrv } from '../../../../core/services/backend_srv';
|
||||
import {
|
||||
LogMessages,
|
||||
logError,
|
||||
logInfo,
|
||||
LogMessages,
|
||||
trackSwitchToPoliciesRouting,
|
||||
trackSwitchToSimplifiedRouting,
|
||||
withPerformanceLogging,
|
||||
withPromRulesMetadataLogging,
|
||||
withRulerRulesMetadataLogging,
|
||||
} from '../Analytics';
|
||||
import { alertRuleApi } from '../api/alertRuleApi';
|
||||
import {
|
||||
deleteAlertManagerConfig,
|
||||
fetchAlertGroups,
|
||||
@ -51,20 +52,20 @@ import { discoverFeatures } from '../api/buildInfo';
|
||||
import { fetchNotifiers } from '../api/grafana';
|
||||
import { FetchPromRulesFilter, fetchRules } from '../api/prometheus';
|
||||
import {
|
||||
FetchRulerRulesFilter,
|
||||
deleteNamespace,
|
||||
deleteRulerRulesGroup,
|
||||
fetchRulerRules,
|
||||
FetchRulerRulesFilter,
|
||||
setRulerRuleGroup,
|
||||
} from '../api/ruler';
|
||||
import { encodeGrafanaNamespace } from '../components/expressions/util';
|
||||
import { RuleFormType, RuleFormValues } from '../types/rule-form';
|
||||
import { addDefaultsToAlertmanagerConfig, removeMuteTimingFromRoute } from '../utils/alertmanager';
|
||||
import {
|
||||
GRAFANA_RULES_SOURCE_NAME,
|
||||
getAllRulesSourceNames,
|
||||
getRulesDataSource,
|
||||
getRulesSourceName,
|
||||
GRAFANA_RULES_SOURCE_NAME,
|
||||
} from '../utils/datasource';
|
||||
import { makeAMLink } from '../utils/misc';
|
||||
import { AsyncRequestMapSlice, withAppEvents, withSerializedError } from '../utils/redux';
|
||||
@ -316,14 +317,6 @@ export function fetchAllPromRulesAction(force = false): ThunkResult<void> {
|
||||
};
|
||||
}
|
||||
|
||||
export const fetchEditableRuleAction = createAsyncThunk(
|
||||
'unifiedalerting/fetchEditableRule',
|
||||
(ruleIdentifier: RuleIdentifier, thunkAPI): Promise<RuleWithLocation | null> => {
|
||||
const rulerConfig = getDataSourceRulerConfig(thunkAPI.getState, ruleIdentifier.ruleSourceName);
|
||||
return withSerializedError(getRulerClient(rulerConfig).findEditableRule(ruleIdentifier));
|
||||
}
|
||||
);
|
||||
|
||||
export function deleteRulesGroupAction(
|
||||
namespace: CombinedRuleNamespace,
|
||||
ruleGroup: CombinedRuleGroup
|
||||
@ -403,6 +396,7 @@ export const saveRuleFormAction = createAsyncThunk(
|
||||
// For the dataSourceName specified
|
||||
// in case of system (cortex/loki)
|
||||
let identifier: RuleIdentifier;
|
||||
|
||||
if (type === RuleFormType.cloudAlerting || type === RuleFormType.cloudRecording) {
|
||||
if (!values.dataSourceName) {
|
||||
throw new Error('The Data source has not been defined.');
|
||||
@ -412,14 +406,14 @@ export const saveRuleFormAction = createAsyncThunk(
|
||||
const rulerClient = getRulerClient(rulerConfig);
|
||||
identifier = await rulerClient.saveLotexRule(values, evaluateEvery, existing);
|
||||
await thunkAPI.dispatch(fetchRulerRulesAction({ rulesSourceName: values.dataSourceName }));
|
||||
|
||||
// in case of grafana managed
|
||||
} else if (type === RuleFormType.grafana) {
|
||||
const rulerConfig = getDataSourceRulerConfig(thunkAPI.getState, GRAFANA_RULES_SOURCE_NAME);
|
||||
const rulerClient = getRulerClient(rulerConfig);
|
||||
identifier = await rulerClient.saveGrafanaRule(values, evaluateEvery, existing);
|
||||
reportSwitchingRoutingType(values, existing);
|
||||
await thunkAPI.dispatch(fetchRulerRulesAction({ rulesSourceName: GRAFANA_RULES_SOURCE_NAME }));
|
||||
// when using a Granfa-managed alert rule we can invalidate a single rule
|
||||
thunkAPI.dispatch(alertRuleApi.util.invalidateTags([{ type: 'GrafanaRulerRule', id: identifier.uid }]));
|
||||
} else {
|
||||
throw new Error('Unexpected rule form type');
|
||||
}
|
||||
@ -439,9 +433,6 @@ export const saveRuleFormAction = createAsyncThunk(
|
||||
const newLocation = `/alerting/${encodeURIComponent(stringifiedIdentifier)}/edit`;
|
||||
if (locationService.getLocation().pathname !== newLocation) {
|
||||
locationService.replace(newLocation);
|
||||
} else {
|
||||
// refresh the details of the current editable rule after saving
|
||||
thunkAPI.dispatch(fetchEditableRuleAction(identifier));
|
||||
}
|
||||
}
|
||||
})()
|
||||
|
@ -5,7 +5,6 @@ import { createAsyncMapSlice, createAsyncSlice } from '../utils/redux';
|
||||
import {
|
||||
deleteAlertManagerConfigAction,
|
||||
fetchAlertGroupsAction,
|
||||
fetchEditableRuleAction,
|
||||
fetchFolderAction,
|
||||
fetchGrafanaAnnotationsAction,
|
||||
fetchGrafanaNotifiersAction,
|
||||
@ -29,7 +28,6 @@ export const reducer = combineReducers({
|
||||
.reducer,
|
||||
ruleForm: combineReducers({
|
||||
saveRule: createAsyncSlice('saveRule', saveRuleFormAction).reducer,
|
||||
existingRule: createAsyncSlice('existingRule', fetchEditableRuleAction).reducer,
|
||||
}),
|
||||
grafanaNotifiers: createAsyncSlice('grafanaNotifiers', fetchGrafanaNotifiersAction).reducer,
|
||||
saveAMConfig: createAsyncSlice('saveAMConfig', updateAlertManagerConfigAction).reducer,
|
||||
|
@ -89,6 +89,7 @@ const grafanaAlert = {
|
||||
condition: 'B',
|
||||
exec_err_state: GrafanaAlertStateDecision.Alerting,
|
||||
namespace_uid: 'namespaceuid123',
|
||||
rule_group: 'my-group',
|
||||
no_data_state: GrafanaAlertStateDecision.NoData,
|
||||
title: 'Test alert',
|
||||
uid: 'asdf23',
|
||||
|
@ -97,6 +97,7 @@ describe('getContactPointsFromDTO', () => {
|
||||
uid: '123',
|
||||
title: 'myalert',
|
||||
namespace_uid: '123',
|
||||
rule_group: 'my-group',
|
||||
condition: 'A',
|
||||
no_data_state: GrafanaAlertStateDecision.Alerting,
|
||||
exec_err_state: GrafanaAlertStateDecision.Alerting,
|
||||
@ -120,6 +121,7 @@ describe('getContactPointsFromDTO', () => {
|
||||
uid: '123',
|
||||
title: 'myalert',
|
||||
namespace_uid: '123',
|
||||
rule_group: 'my-group',
|
||||
condition: 'A',
|
||||
no_data_state: GrafanaAlertStateDecision.Alerting,
|
||||
exec_err_state: GrafanaAlertStateDecision.Alerting,
|
||||
|
@ -46,6 +46,7 @@ describe('hashRulerRule', () => {
|
||||
const grafanaAlertDefinition: GrafanaRuleDefinition = {
|
||||
uid: RULE_UID,
|
||||
namespace_uid: 'namespace',
|
||||
rule_group: 'my-group',
|
||||
title: 'my rule',
|
||||
condition: '',
|
||||
data: [],
|
||||
|
@ -1,4 +1,9 @@
|
||||
import { RuleIdentifier, RulerDataSourceConfig, RuleWithLocation } from 'app/types/unified-alerting';
|
||||
import {
|
||||
GrafanaRuleIdentifier,
|
||||
RuleIdentifier,
|
||||
RulerDataSourceConfig,
|
||||
RuleWithLocation,
|
||||
} from 'app/types/unified-alerting';
|
||||
import {
|
||||
PostableRuleGrafanaRuleDTO,
|
||||
PostableRulerRuleGroupDTO,
|
||||
@ -21,9 +26,16 @@ import {
|
||||
|
||||
export interface RulerClient {
|
||||
findEditableRule(ruleIdentifier: RuleIdentifier): Promise<RuleWithLocation | null>;
|
||||
|
||||
deleteRule(ruleWithLocation: RuleWithLocation): Promise<void>;
|
||||
|
||||
saveLotexRule(values: RuleFormValues, evaluateEvery: string, existing?: RuleWithLocation): Promise<RuleIdentifier>;
|
||||
saveGrafanaRule(values: RuleFormValues, evaluateEvery: string, existing?: RuleWithLocation): Promise<RuleIdentifier>;
|
||||
|
||||
saveGrafanaRule(
|
||||
values: RuleFormValues,
|
||||
evaluateEvery: string,
|
||||
existing?: RuleWithLocation
|
||||
): Promise<GrafanaRuleIdentifier>;
|
||||
}
|
||||
|
||||
export function getRulerClient(rulerConfig: RulerDataSourceConfig): RulerClient {
|
||||
@ -153,7 +165,7 @@ export function getRulerClient(rulerConfig: RulerDataSourceConfig): RulerClient
|
||||
values: RuleFormValues,
|
||||
evaluateEvery: string,
|
||||
existingRule?: RuleWithLocation
|
||||
): Promise<RuleIdentifier> => {
|
||||
): Promise<GrafanaRuleIdentifier> => {
|
||||
const { folder, group } = values;
|
||||
if (!folder) {
|
||||
throw new Error('Folder must be specified');
|
||||
@ -190,7 +202,7 @@ export function getRulerClient(rulerConfig: RulerDataSourceConfig): RulerClient
|
||||
namespaceUID: string,
|
||||
group: { name: string; interval: string },
|
||||
newRule: PostableRuleGrafanaRuleDTO
|
||||
): Promise<RuleIdentifier> => {
|
||||
): Promise<GrafanaRuleIdentifier> => {
|
||||
const existingGroup = await fetchRulerRulesGroup(rulerConfig, namespaceUID, group.name);
|
||||
if (!existingGroup) {
|
||||
throw new Error(`No group found with name "${group.name}"`);
|
||||
@ -213,7 +225,7 @@ export function getRulerClient(rulerConfig: RulerDataSourceConfig): RulerClient
|
||||
group: { name: string; interval: string },
|
||||
existingRule: RuleWithLocation,
|
||||
newRule: PostableRuleGrafanaRuleDTO
|
||||
): Promise<RuleIdentifier> => {
|
||||
): Promise<GrafanaRuleIdentifier> => {
|
||||
// make sure our updated alert has the same UID as before
|
||||
// that way the rule is automatically moved to the new namespace / group name
|
||||
copyGrafanaUID(existingRule, newRule);
|
||||
@ -228,7 +240,7 @@ export function getRulerClient(rulerConfig: RulerDataSourceConfig): RulerClient
|
||||
existingRule: RuleWithLocation,
|
||||
newRule: PostableRuleGrafanaRuleDTO,
|
||||
interval: string
|
||||
): Promise<RuleIdentifier> => {
|
||||
): Promise<GrafanaRuleIdentifier> => {
|
||||
// make sure our updated alert has the same UID as before
|
||||
copyGrafanaUID(existingRule, newRule);
|
||||
|
||||
|
@ -44,6 +44,7 @@ export function getRulerRulesResponse(folderName: string, folderUid: string, see
|
||||
],
|
||||
uid: random.guid(),
|
||||
namespace_uid: folderUid,
|
||||
rule_group: 'my-group',
|
||||
no_data_state: GrafanaAlertStateDecision.NoData,
|
||||
exec_err_state: GrafanaAlertStateDecision.Error,
|
||||
is_paused: false,
|
||||
|
@ -221,6 +221,7 @@ export interface GrafanaRuleDefinition extends PostableGrafanaRuleDefinition {
|
||||
id?: string;
|
||||
uid: string;
|
||||
namespace_uid: string;
|
||||
rule_group: string;
|
||||
provenance?: string;
|
||||
}
|
||||
|
||||
|
@ -139,9 +139,9 @@ export interface CombinedRuleNamespace {
|
||||
export interface RuleWithLocation<T = RulerRuleDTO> {
|
||||
ruleSourceName: string;
|
||||
namespace: string;
|
||||
namespace_uid?: string; // Grafana folder UID
|
||||
group: RulerRuleGroupDTO;
|
||||
rule: T;
|
||||
namespace_uid?: string;
|
||||
}
|
||||
|
||||
export interface CombinedRuleWithLocation extends CombinedRule {
|
||||
|
@ -10,6 +10,7 @@ import RuleEditor from 'app/features/alerting/unified/RuleEditor';
|
||||
import { TestProvider } from './TestProvider';
|
||||
|
||||
export const ui = {
|
||||
loadingIndicator: byText('Loading rule...'),
|
||||
inputs: {
|
||||
name: byRole('textbox', { name: 'name' }),
|
||||
alertType: byTestId('alert-type-picker'),
|
||||
|
Loading…
Reference in New Issue
Block a user