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 { render, waitFor, waitForElementToBeRemoved, within } from '@testing-library/react';
|
||||||
import { setupServer } from 'msw/node';
|
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { FormProvider, useForm } from 'react-hook-form';
|
import { FormProvider, useForm } from 'react-hook-form';
|
||||||
import { TestProvider } from 'test/helpers/TestProvider';
|
import { TestProvider } from 'test/helpers/TestProvider';
|
||||||
import { byRole, byTestId, byText } from 'testing-library-selector';
|
import { byRole, byTestId, byText } from 'testing-library-selector';
|
||||||
|
|
||||||
import { selectors } from '@grafana/e2e-selectors/src';
|
import { selectors } from '@grafana/e2e-selectors/src';
|
||||||
import { config, setBackendSrv, setDataSourceSrv } from '@grafana/runtime';
|
import { setDataSourceSrv } from '@grafana/runtime';
|
||||||
import { backendSrv } from 'app/core/services/backend_srv';
|
|
||||||
import { DashboardSearchItem, DashboardSearchItemType } from 'app/features/search/types';
|
import { DashboardSearchItem, DashboardSearchItemType } from 'app/features/search/types';
|
||||||
import { AlertManagerCortexConfig } from 'app/plugins/datasource/alertmanager/types';
|
import { AlertManagerCortexConfig } from 'app/plugins/datasource/alertmanager/types';
|
||||||
import { RuleWithLocation } from 'app/types/unified-alerting';
|
import { RuleWithLocation } from 'app/types/unified-alerting';
|
||||||
|
|
||||||
|
import { AccessControlAction } from '../../../types';
|
||||||
import {
|
import {
|
||||||
RulerAlertingRuleDTO,
|
RulerAlertingRuleDTO,
|
||||||
RulerGrafanaRuleDTO,
|
RulerGrafanaRuleDTO,
|
||||||
@ -21,19 +20,21 @@ import {
|
|||||||
|
|
||||||
import { cloneRuleDefinition, CloneRuleEditor } from './CloneRuleEditor';
|
import { cloneRuleDefinition, CloneRuleEditor } from './CloneRuleEditor';
|
||||||
import { ExpressionEditorProps } from './components/rule-editor/ExpressionEditor';
|
import { ExpressionEditorProps } from './components/rule-editor/ExpressionEditor';
|
||||||
import { mockApi, mockSearchApi } from './mockApi';
|
import { mockFeatureDiscoveryApi, mockSearchApi, setupMswServer } from './mockApi';
|
||||||
import {
|
import {
|
||||||
labelsPluginMetaMock,
|
grantUserPermissions,
|
||||||
mockDataSource,
|
mockDataSource,
|
||||||
MockDataSourceSrv,
|
MockDataSourceSrv,
|
||||||
mockRulerAlertingRule,
|
mockRulerAlertingRule,
|
||||||
mockRulerGrafanaRule,
|
mockRulerGrafanaRule,
|
||||||
mockRulerRuleGroup,
|
mockRulerRuleGroup,
|
||||||
mockStore,
|
|
||||||
} from './mocks';
|
} from './mocks';
|
||||||
|
import { grafanaRulerRule } from './mocks/alertRuleApi';
|
||||||
import { mockAlertmanagerConfigResponse } from './mocks/alertmanagerApi';
|
import { mockAlertmanagerConfigResponse } from './mocks/alertmanagerApi';
|
||||||
import { mockRulerRulesApiResponse, mockRulerRulesGroupApiResponse } from './mocks/rulerApi';
|
import { mockRulerRulesApiResponse, mockRulerRulesGroupApiResponse } from './mocks/rulerApi';
|
||||||
import { AlertingQueryRunner } from './state/AlertingQueryRunner';
|
import { AlertingQueryRunner } from './state/AlertingQueryRunner';
|
||||||
|
import { setupDataSources } from './testSetup/datasources';
|
||||||
|
import { buildInfoResponse } from './testSetup/featureDiscovery';
|
||||||
import { RuleFormValues } from './types/rule-form';
|
import { RuleFormValues } from './types/rule-form';
|
||||||
import { Annotation } from './utils/constants';
|
import { Annotation } from './utils/constants';
|
||||||
import { GRAFANA_RULES_SOURCE_NAME } from './utils/datasource';
|
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());
|
jest.spyOn(AlertingQueryRunner.prototype, 'run').mockImplementation(() => Promise.resolve());
|
||||||
|
|
||||||
const server = setupServer();
|
const server = setupMswServer();
|
||||||
|
|
||||||
beforeAll(() => {
|
|
||||||
setBackendSrv(backendSrv);
|
|
||||||
server.listen({ onUnhandledRequest: 'error' });
|
|
||||||
});
|
|
||||||
|
|
||||||
beforeEach(() => {
|
|
||||||
server.resetHandlers();
|
|
||||||
});
|
|
||||||
|
|
||||||
afterAll(() => {
|
|
||||||
server.close();
|
|
||||||
});
|
|
||||||
|
|
||||||
const ui = {
|
const ui = {
|
||||||
inputs: {
|
inputs: {
|
||||||
@ -80,46 +68,16 @@ const ui = {
|
|||||||
annotationValue: (idx: number) => byTestId(`annotation-value-${idx}`),
|
annotationValue: (idx: number) => byTestId(`annotation-value-${idx}`),
|
||||||
labelValue: (idx: number) => byTestId(`label-value-${idx}`),
|
labelValue: (idx: number) => byTestId(`label-value-${idx}`),
|
||||||
},
|
},
|
||||||
loadingIndicator: byText('Loading the rule'),
|
loadingIndicator: byText('Loading the rule...'),
|
||||||
};
|
};
|
||||||
|
|
||||||
function getProvidersWrapper() {
|
function Wrapper({ children }: React.PropsWithChildren<{}>) {
|
||||||
return function Wrapper({ children }: React.PropsWithChildren<{}>) {
|
const formApi = useForm<RuleFormValues>({ defaultValues: getDefaultFormValues() });
|
||||||
const store = mockStore((store) => {
|
return (
|
||||||
store.unifiedAlerting.dataSources['grafana'] = {
|
<TestProvider>
|
||||||
loading: false,
|
<FormProvider {...formApi}>{children}</FormProvider>
|
||||||
dispatched: true,
|
</TestProvider>
|
||||||
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>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const amConfig: AlertManagerCortexConfig = {
|
const amConfig: AlertManagerCortexConfig = {
|
||||||
@ -139,33 +97,26 @@ const amConfig: AlertManagerCortexConfig = {
|
|||||||
template_files: {},
|
template_files: {},
|
||||||
};
|
};
|
||||||
|
|
||||||
mockApi(server).plugins.getPluginSettings({ ...labelsPluginMetaMock, enabled: false });
|
|
||||||
describe('CloneRuleEditor', function () {
|
describe('CloneRuleEditor', function () {
|
||||||
|
grantUserPermissions([AccessControlAction.AlertingRuleExternalRead]);
|
||||||
|
|
||||||
describe('Grafana-managed rules', function () {
|
describe('Grafana-managed rules', function () {
|
||||||
it('should populate form values from the existing alert rule', async function () {
|
it('should populate form values from the existing alert rule', async function () {
|
||||||
setDataSourceSrv(new MockDataSourceSrv({}));
|
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([
|
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);
|
mockAlertmanagerConfigResponse(server, GRAFANA_RULES_SOURCE_NAME, amConfig);
|
||||||
|
|
||||||
render(<CloneRuleEditor sourceRuleId={{ uid: 'grafana-rule-1', ruleSourceName: 'grafana' }} />, {
|
render(
|
||||||
wrapper: getProvidersWrapper(),
|
<CloneRuleEditor sourceRuleId={{ uid: grafanaRulerRule.grafana_alert.uid, ruleSourceName: 'grafana' }} />,
|
||||||
});
|
{ wrapper: Wrapper }
|
||||||
|
);
|
||||||
|
|
||||||
await waitForElementToBeRemoved(ui.loadingIndicator.query());
|
await waitForElementToBeRemoved(ui.loadingIndicator.query());
|
||||||
await waitFor(() => {
|
await waitFor(() => {
|
||||||
@ -173,9 +124,9 @@ describe('CloneRuleEditor', function () {
|
|||||||
});
|
});
|
||||||
|
|
||||||
await waitFor(() => {
|
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.folderContainer.get()).toHaveTextContent('folder-one');
|
||||||
expect(ui.inputs.group.get()).toHaveTextContent('group1');
|
expect(ui.inputs.group.get()).toHaveTextContent(grafanaRulerRule.grafana_alert.rule_group);
|
||||||
expect(
|
expect(
|
||||||
byRole('listitem', {
|
byRole('listitem', {
|
||||||
name: 'severity: critical',
|
name: 'severity: critical',
|
||||||
@ -186,7 +137,7 @@ describe('CloneRuleEditor', function () {
|
|||||||
name: 'region: nasa',
|
name: 'region: nasa',
|
||||||
}).get()
|
}).get()
|
||||||
).toBeInTheDocument();
|
).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',
|
name: 'my-prom-ds',
|
||||||
uid: 'my-prom-ds',
|
uid: 'my-prom-ds',
|
||||||
});
|
});
|
||||||
config.datasources = {
|
setupDataSources(dsSettings);
|
||||||
'my-prom-ds': dsSettings,
|
mockFeatureDiscoveryApi(server).discoverDsFeatures(dsSettings, buildInfoResponse.mimir);
|
||||||
};
|
|
||||||
|
|
||||||
setDataSourceSrv(new MockDataSourceSrv({ 'my-prom-ds': dsSettings }));
|
|
||||||
|
|
||||||
const originRule = mockRulerAlertingRule({
|
const originRule = mockRulerAlertingRule({
|
||||||
for: '1m',
|
for: '1m',
|
||||||
alert: 'First Ruler Rule',
|
alert: 'First Ruler Rule',
|
||||||
@ -242,9 +189,7 @@ describe('CloneRuleEditor', function () {
|
|||||||
rulerRuleHash: hashRulerRule(originRule),
|
rulerRuleHash: hashRulerRule(originRule),
|
||||||
}}
|
}}
|
||||||
/>,
|
/>,
|
||||||
{
|
{ wrapper: Wrapper }
|
||||||
wrapper: getProvidersWrapper(),
|
|
||||||
}
|
|
||||||
);
|
);
|
||||||
|
|
||||||
await waitForElementToBeRemoved(ui.loadingIndicator.query());
|
await waitForElementToBeRemoved(ui.loadingIndicator.query());
|
||||||
|
@ -1,32 +1,25 @@
|
|||||||
import { cloneDeep } from 'lodash';
|
import { cloneDeep } from 'lodash';
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { useAsync } from 'react-use';
|
|
||||||
|
|
||||||
import { locationService } from '@grafana/runtime/src';
|
import { locationService } from '@grafana/runtime/src';
|
||||||
import { Alert, LoadingPlaceholder } from '@grafana/ui/src';
|
import { Alert, LoadingPlaceholder } from '@grafana/ui/src';
|
||||||
|
|
||||||
import { useDispatch } from '../../../types';
|
|
||||||
import { RuleIdentifier, RuleWithLocation } from '../../../types/unified-alerting';
|
import { RuleIdentifier, RuleWithLocation } from '../../../types/unified-alerting';
|
||||||
import { RulerRuleDTO } from '../../../types/unified-alerting-dto';
|
import { RulerRuleDTO } from '../../../types/unified-alerting-dto';
|
||||||
|
|
||||||
import { AlertRuleForm } from './components/rule-editor/alert-rule-form/AlertRuleForm';
|
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 { generateCopiedName } from './utils/duplicate';
|
||||||
|
import { stringifyErrorLike } from './utils/misc';
|
||||||
import { rulerRuleToFormValues } from './utils/rule-form';
|
import { rulerRuleToFormValues } from './utils/rule-form';
|
||||||
import { getRuleName, isAlertingRulerRule, isGrafanaRulerRule, isRecordingRulerRule } from './utils/rules';
|
import { getRuleName, isAlertingRulerRule, isGrafanaRulerRule, isRecordingRulerRule } from './utils/rules';
|
||||||
import { createUrl } from './utils/url';
|
import { createUrl } from './utils/url';
|
||||||
|
|
||||||
export function CloneRuleEditor({ sourceRuleId }: { sourceRuleId: RuleIdentifier }) {
|
export function CloneRuleEditor({ sourceRuleId }: { sourceRuleId: RuleIdentifier }) {
|
||||||
const dispatch = useDispatch();
|
const { loading, result: rule, error } = useRuleWithLocation({ ruleIdentifier: sourceRuleId });
|
||||||
|
|
||||||
const {
|
|
||||||
loading,
|
|
||||||
value: rule,
|
|
||||||
error,
|
|
||||||
} = useAsync(() => dispatch(fetchEditableRuleAction(sourceRuleId)).unwrap(), [sourceRuleId]);
|
|
||||||
|
|
||||||
if (loading) {
|
if (loading) {
|
||||||
return <LoadingPlaceholder text="Loading the rule" />;
|
return <LoadingPlaceholder text="Loading the rule..." />;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (rule) {
|
if (rule) {
|
||||||
@ -39,7 +32,7 @@ export function CloneRuleEditor({ sourceRuleId }: { sourceRuleId: RuleIdentifier
|
|||||||
if (error) {
|
if (error) {
|
||||||
return (
|
return (
|
||||||
<Alert title="Error" severity="error">
|
<Alert title="Error" severity="error">
|
||||||
{error.message}
|
{stringifyErrorLike(error)}
|
||||||
</Alert>
|
</Alert>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -1,16 +1,13 @@
|
|||||||
import React, { useEffect } from 'react';
|
import React from 'react';
|
||||||
|
|
||||||
import { Alert, LoadingPlaceholder } from '@grafana/ui';
|
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 { RuleIdentifier } from 'app/types/unified-alerting';
|
||||||
|
|
||||||
import { AlertWarning } from './AlertWarning';
|
import { AlertWarning } from './AlertWarning';
|
||||||
import { AlertRuleForm } from './components/rule-editor/alert-rule-form/AlertRuleForm';
|
import { AlertRuleForm } from './components/rule-editor/alert-rule-form/AlertRuleForm';
|
||||||
|
import { useRuleWithLocation } from './hooks/useCombinedRule';
|
||||||
import { useIsRuleEditable } from './hooks/useIsRuleEditable';
|
import { useIsRuleEditable } from './hooks/useIsRuleEditable';
|
||||||
import { useUnifiedAlertingSelector } from './hooks/useUnifiedAlertingSelector';
|
import { stringifyErrorLike } from './utils/misc';
|
||||||
import { fetchEditableRuleAction } from './state/actions';
|
|
||||||
import { initialAsyncRequestState } from './utils/redux';
|
|
||||||
import * as ruleId from './utils/rule-id';
|
import * as ruleId from './utils/rule-id';
|
||||||
|
|
||||||
interface ExistingRuleEditorProps {
|
interface ExistingRuleEditorProps {
|
||||||
@ -19,42 +16,31 @@ interface ExistingRuleEditorProps {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function ExistingRuleEditor({ identifier, id }: ExistingRuleEditorProps) {
|
export function ExistingRuleEditor({ identifier, id }: ExistingRuleEditorProps) {
|
||||||
useCleanup((state) => (state.unifiedAlerting.ruleForm.existingRule = initialAsyncRequestState));
|
|
||||||
|
|
||||||
const {
|
const {
|
||||||
loading: loadingAlertRule,
|
loading: loadingAlertRule,
|
||||||
result,
|
result: ruleWithLocation,
|
||||||
error,
|
error,
|
||||||
dispatched,
|
} = useRuleWithLocation({ ruleIdentifier: identifier });
|
||||||
} = useUnifiedAlertingSelector((state) => state.ruleForm.existingRule);
|
|
||||||
|
|
||||||
const dispatch = useDispatch();
|
const ruleSourceName = ruleId.ruleIdentifierToRuleSourceName(identifier);
|
||||||
const { isEditable, loading: loadingEditable } = useIsRuleEditable(
|
|
||||||
ruleId.ruleIdentifierToRuleSourceName(identifier),
|
const { isEditable, loading: loadingEditable } = useIsRuleEditable(ruleSourceName, ruleWithLocation?.rule);
|
||||||
result?.rule
|
|
||||||
);
|
|
||||||
|
|
||||||
const loading = loadingAlertRule || loadingEditable;
|
const loading = loadingAlertRule || loadingEditable;
|
||||||
|
|
||||||
useEffect(() => {
|
if (loading) {
|
||||||
if (!dispatched) {
|
|
||||||
dispatch(fetchEditableRuleAction(identifier));
|
|
||||||
}
|
|
||||||
}, [dispatched, dispatch, identifier]);
|
|
||||||
|
|
||||||
if (loading || isEditable === undefined) {
|
|
||||||
return <LoadingPlaceholder text="Loading rule..." />;
|
return <LoadingPlaceholder text="Loading rule..." />;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (error) {
|
if (error) {
|
||||||
return (
|
return (
|
||||||
<Alert severity="error" title="Failed to load rule">
|
<Alert severity="error" title="Failed to load rule">
|
||||||
{error.message}
|
{stringifyErrorLike(error)}
|
||||||
</Alert>
|
</Alert>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!result) {
|
if (!ruleWithLocation) {
|
||||||
return <AlertWarning title="Rule not found">Sorry! This rule does not exist.</AlertWarning>;
|
return <AlertWarning title="Rule not found">Sorry! This rule does not exist.</AlertWarning>;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -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 <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 { TestProvider } from 'test/helpers/TestProvider';
|
||||||
import { ui } from 'test/helpers/alertingRuleEditor';
|
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 { contextSrv } from 'app/core/services/context_srv';
|
||||||
import { DashboardSearchHit, DashboardSearchItemType } from 'app/features/search/types';
|
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 { searchFolders } from '../../../../app/features/manage-dashboards/state/actions';
|
||||||
import { backendSrv } from '../../../core/services/backend_srv';
|
import { backendSrv } from '../../../core/services/backend_srv';
|
||||||
import { AccessControlAction } from '../../../types';
|
import { AccessControlAction } from '../../../types';
|
||||||
|
|
||||||
import RuleEditor from './RuleEditor';
|
import RuleEditor from './RuleEditor';
|
||||||
import { discoverFeatures } from './api/buildInfo';
|
import * as ruler from './api/ruler';
|
||||||
import { fetchRulerRules, fetchRulerRulesGroup, fetchRulerRulesNamespace, setRulerRuleGroup } from './api/ruler';
|
|
||||||
import { ExpressionEditorProps } from './components/rule-editor/ExpressionEditor';
|
import { ExpressionEditorProps } from './components/rule-editor/ExpressionEditor';
|
||||||
import { MockDataSourceSrv, grantUserPermissions, mockDataSource, mockFolder } from './mocks';
|
import { setupMswServer } from './mockApi';
|
||||||
import { fetchRulerRulesIfNotFetchedYet } from './state/actions';
|
import { grantUserPermissions, mockDataSource, mockFolder } from './mocks';
|
||||||
import * as config from './utils/config';
|
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 { GRAFANA_RULES_SOURCE_NAME } from './utils/datasource';
|
||||||
import { getDefaultQueries } from './utils/rule-form';
|
|
||||||
|
|
||||||
jest.mock('./components/rule-editor/ExpressionEditor', () => ({
|
jest.mock('./components/rule-editor/ExpressionEditor', () => ({
|
||||||
// eslint-disable-next-line react/display-name
|
|
||||||
ExpressionEditor: ({ value, onChange }: ExpressionEditorProps) => (
|
ExpressionEditor: ({ value, onChange }: ExpressionEditorProps) => (
|
||||||
<input value={value} data-testid="expr" onChange={(e) => onChange(e.target.value)} />
|
<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>,
|
AppChromeUpdate: ({ actions }: { actions: React.ReactNode }) => <div>{actions}</div>,
|
||||||
}));
|
}));
|
||||||
|
|
||||||
jest.mock('./api/buildInfo');
|
|
||||||
jest.mock('./api/ruler');
|
|
||||||
jest.mock('../../../../app/features/manage-dashboards/state/actions');
|
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.
|
// there's no angular scope in test and things go terribly wrong when trying to render the query editor row.
|
||||||
// lets just skip it
|
// lets just skip it
|
||||||
jest.mock('app/features/query/components/QueryEditorRow', () => ({
|
jest.mock('app/features/query/components/QueryEditorRow', () => ({
|
||||||
// eslint-disable-next-line react/display-name
|
|
||||||
QueryEditorRow: () => <p>hi</p>,
|
QueryEditorRow: () => <p>hi</p>,
|
||||||
}));
|
}));
|
||||||
|
|
||||||
jest.spyOn(config, 'getAllDataSources');
|
|
||||||
|
|
||||||
jest.setTimeout(60 * 1000);
|
jest.setTimeout(60 * 1000);
|
||||||
|
|
||||||
const mocks = {
|
const mocks = {
|
||||||
getAllDataSources: jest.mocked(config.getAllDataSources),
|
|
||||||
searchFolders: jest.mocked(searchFolders),
|
searchFolders: jest.mocked(searchFolders),
|
||||||
api: {
|
api: {
|
||||||
discoverFeatures: jest.mocked(discoverFeatures),
|
setRulerRuleGroup: jest.spyOn(ruler, 'setRulerRuleGroup'),
|
||||||
fetchRulerRulesGroup: jest.mocked(fetchRulerRulesGroup),
|
|
||||||
setRulerRuleGroup: jest.mocked(setRulerRuleGroup),
|
|
||||||
fetchRulerRulesNamespace: jest.mocked(fetchRulerRulesNamespace),
|
|
||||||
fetchRulerRules: jest.mocked(fetchRulerRules),
|
|
||||||
fetchRulerRulesIfNotFetchedYet: jest.mocked(fetchRulerRulesIfNotFetchedYet),
|
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
setupMswServer();
|
||||||
|
|
||||||
function renderRuleEditor(identifier?: string) {
|
function renderRuleEditor(identifier?: string) {
|
||||||
locationService.push(identifier ? `/alerting/${identifier}/edit` : `/alerting/new`);
|
locationService.push(identifier ? `/alerting/${identifier}/edit` : `/alerting/new`);
|
||||||
|
|
||||||
@ -95,10 +84,9 @@ describe('RuleEditor grafana managed rules', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('can edit grafana managed rule', async () => {
|
it('can edit grafana managed rule', async () => {
|
||||||
const uid = 'FOOBAR123';
|
|
||||||
const folder = {
|
const folder = {
|
||||||
title: 'Folder A',
|
title: 'Folder A',
|
||||||
uid: 'abcd',
|
uid: grafanaRulerRule.grafana_alert.namespace_uid,
|
||||||
id: 1,
|
id: 1,
|
||||||
type: DashboardSearchItemType.DashDB,
|
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.setRulerRuleGroup.mockResolvedValue();
|
||||||
mocks.api.fetchRulerRulesNamespace.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.searchFolders.mockResolvedValue([folder, slashedFolder] as DashboardSearchHit[]);
|
mocks.searchFolders.mockResolvedValue([folder, slashedFolder] as DashboardSearchHit[]);
|
||||||
|
|
||||||
renderRuleEditor(uid);
|
renderRuleEditor(grafanaRulerRule.grafana_alert.uid);
|
||||||
|
|
||||||
// check that it's filled in
|
// check that it's filled in
|
||||||
const nameInput = await ui.inputs.name.find();
|
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
|
//check that folder is in the list
|
||||||
expect(ui.inputs.folder.get()).toHaveTextContent(new RegExp(folder.title));
|
expect(ui.inputs.folder.get()).toHaveTextContent(new RegExp(folder.title));
|
||||||
expect(ui.inputs.annotationValue(0).get()).toHaveValue('some summary');
|
expect(ui.inputs.annotationValue(0).get()).toHaveValue(grafanaRulerRule.annotations[Annotation.summary]);
|
||||||
expect(ui.inputs.annotationValue(1).get()).toHaveValue('some description');
|
|
||||||
|
|
||||||
//check that slashed folders are not in the list
|
//check that slashed folders are not in the list
|
||||||
expect(ui.inputs.folder.get()).toHaveTextContent(new RegExp(folder.title));
|
expect(ui.inputs.folder.get()).toHaveTextContent(new RegExp(folder.title));
|
||||||
@ -195,25 +157,15 @@ describe('RuleEditor grafana managed rules', () => {
|
|||||||
|
|
||||||
expect(mocks.api.setRulerRuleGroup).toHaveBeenCalledWith(
|
expect(mocks.api.setRulerRuleGroup).toHaveBeenCalledWith(
|
||||||
{ dataSourceName: GRAFANA_RULES_SOURCE_NAME, apiVersion: 'legacy' },
|
{ dataSourceName: GRAFANA_RULES_SOURCE_NAME, apiVersion: 'legacy' },
|
||||||
'abcd',
|
grafanaRulerRule.grafana_alert.namespace_uid,
|
||||||
{
|
{
|
||||||
interval: '1m',
|
interval: grafanaRulerGroup.interval,
|
||||||
name: 'group1',
|
name: grafanaRulerGroup.name,
|
||||||
rules: [
|
rules: [
|
||||||
{
|
{
|
||||||
annotations: { description: 'some description', summary: 'some summary', custom: 'value' },
|
...grafanaRulerRule,
|
||||||
labels: { severity: 'warn', team: 'the a-team' },
|
annotations: { ...grafanaRulerRule.annotations, custom: 'value' },
|
||||||
for: '1m',
|
grafana_alert: { ...grafanaRulerRule.grafana_alert, namespace_uid: undefined, rule_group: undefined },
|
||||||
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',
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
}
|
}
|
||||||
|
@ -5,33 +5,30 @@ import { renderRuleEditor, ui } from 'test/helpers/alertingRuleEditor';
|
|||||||
import { clickSelectOption } from 'test/helpers/selectOptionInTest';
|
import { clickSelectOption } from 'test/helpers/selectOptionInTest';
|
||||||
import { byRole } from 'testing-library-selector';
|
import { byRole } from 'testing-library-selector';
|
||||||
|
|
||||||
import { setDataSourceSrv } from '@grafana/runtime';
|
|
||||||
import { contextSrv } from 'app/core/services/context_srv';
|
import { contextSrv } from 'app/core/services/context_srv';
|
||||||
import { setupMswServer } from 'app/features/alerting/unified/mockApi';
|
import { setupMswServer } from 'app/features/alerting/unified/mockApi';
|
||||||
import { DashboardSearchHit, DashboardSearchItemType } from 'app/features/search/types';
|
import { DashboardSearchHit, DashboardSearchItemType } from 'app/features/search/types';
|
||||||
import { AccessControlAction } from 'app/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 { searchFolders } from '../../../../app/features/manage-dashboards/state/actions';
|
||||||
|
|
||||||
import { discoverFeatures } from './api/buildInfo';
|
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 { ExpressionEditorProps } from './components/rule-editor/ExpressionEditor';
|
||||||
import { MockDataSourceSrv, grantUserPermissions, mockDataSource } from './mocks';
|
import { grantUserPermissions, mockDataSource } from './mocks';
|
||||||
import { fetchRulerRulesIfNotFetchedYet } from './state/actions';
|
import { grafanaRulerGroup, grafanaRulerRule } from './mocks/alertRuleApi';
|
||||||
|
import { setupDataSources } from './testSetup/datasources';
|
||||||
import * as config from './utils/config';
|
import * as config from './utils/config';
|
||||||
import { GRAFANA_RULES_SOURCE_NAME } from './utils/datasource';
|
import { GRAFANA_RULES_SOURCE_NAME } from './utils/datasource';
|
||||||
import { getDefaultQueries } from './utils/rule-form';
|
import { getDefaultQueries } from './utils/rule-form';
|
||||||
|
|
||||||
jest.mock('./components/rule-editor/ExpressionEditor', () => ({
|
jest.mock('./components/rule-editor/ExpressionEditor', () => ({
|
||||||
// eslint-disable-next-line react/display-name
|
|
||||||
ExpressionEditor: ({ value, onChange }: ExpressionEditorProps) => (
|
ExpressionEditor: ({ value, onChange }: ExpressionEditorProps) => (
|
||||||
<input value={value} data-testid="expr" onChange={(e) => onChange(e.target.value)} />
|
<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/features/manage-dashboards/state/actions');
|
||||||
|
|
||||||
jest.mock('app/core/components/AppChrome/AppChromeUpdate', () => ({
|
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.
|
// there's no angular scope in test and things go terribly wrong when trying to render the query editor row.
|
||||||
// lets just skip it
|
// lets just skip it
|
||||||
jest.mock('app/features/query/components/QueryEditorRow', () => ({
|
jest.mock('app/features/query/components/QueryEditorRow', () => ({
|
||||||
// eslint-disable-next-line react/display-name
|
|
||||||
QueryEditorRow: () => <p>hi</p>,
|
QueryEditorRow: () => <p>hi</p>,
|
||||||
}));
|
}));
|
||||||
|
|
||||||
@ -54,11 +50,7 @@ const mocks = {
|
|||||||
searchFolders: jest.mocked(searchFolders),
|
searchFolders: jest.mocked(searchFolders),
|
||||||
api: {
|
api: {
|
||||||
discoverFeatures: jest.mocked(discoverFeatures),
|
discoverFeatures: jest.mocked(discoverFeatures),
|
||||||
fetchRulerRulesGroup: jest.mocked(fetchRulerRulesGroup),
|
setRulerRuleGroup: jest.spyOn(ruler, 'setRulerRuleGroup'),
|
||||||
setRulerRuleGroup: jest.mocked(setRulerRuleGroup),
|
|
||||||
fetchRulerRulesNamespace: jest.mocked(fetchRulerRulesNamespace),
|
|
||||||
fetchRulerRules: jest.mocked(fetchRulerRules),
|
|
||||||
fetchRulerRulesIfNotFetchedYet: jest.mocked(fetchRulerRulesIfNotFetchedYet),
|
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -96,64 +88,13 @@ describe('RuleEditor grafana managed rules', () => {
|
|||||||
),
|
),
|
||||||
};
|
};
|
||||||
|
|
||||||
setDataSourceSrv(new MockDataSourceSrv(dataSources));
|
setupDataSources(dataSources.default);
|
||||||
mocks.getAllDataSources.mockReturnValue(Object.values(dataSources));
|
mocks.getAllDataSources.mockReturnValue(Object.values(dataSources));
|
||||||
mocks.api.setRulerRuleGroup.mockResolvedValue();
|
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([
|
mocks.searchFolders.mockResolvedValue([
|
||||||
{
|
{
|
||||||
title: 'Folder A',
|
title: 'Folder A',
|
||||||
uid: 'abcd',
|
uid: grafanaRulerRule.grafana_alert.namespace_uid,
|
||||||
id: 1,
|
id: 1,
|
||||||
type: DashboardSearchItemType.DashDB,
|
type: DashboardSearchItemType.DashDB,
|
||||||
},
|
},
|
||||||
@ -171,12 +112,6 @@ describe('RuleEditor grafana managed rules', () => {
|
|||||||
},
|
},
|
||||||
] as DashboardSearchHit[]);
|
] as DashboardSearchHit[]);
|
||||||
|
|
||||||
mocks.api.discoverFeatures.mockResolvedValue({
|
|
||||||
application: PromApplication.Prometheus,
|
|
||||||
features: {
|
|
||||||
rulerApiEnabled: false,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
renderRuleEditor();
|
renderRuleEditor();
|
||||||
await waitForElementToBeRemoved(screen.getAllByTestId('Spinner'));
|
await waitForElementToBeRemoved(screen.getAllByTestId('Spinner'));
|
||||||
|
|
||||||
@ -186,7 +121,7 @@ describe('RuleEditor grafana managed rules', () => {
|
|||||||
await clickSelectOption(folderInput, 'Folder A');
|
await clickSelectOption(folderInput, 'Folder A');
|
||||||
const groupInput = await ui.inputs.group.find();
|
const groupInput = await ui.inputs.group.find();
|
||||||
await userEvent.click(byRole('combobox').get(groupInput));
|
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');
|
await userEvent.type(ui.inputs.annotationValue(1).get(), 'some description');
|
||||||
|
|
||||||
// save and check what was sent to backend
|
// save and check what was sent to backend
|
||||||
@ -196,11 +131,12 @@ describe('RuleEditor grafana managed rules', () => {
|
|||||||
// 9seg
|
// 9seg
|
||||||
expect(mocks.api.setRulerRuleGroup).toHaveBeenCalledWith(
|
expect(mocks.api.setRulerRuleGroup).toHaveBeenCalledWith(
|
||||||
{ dataSourceName: GRAFANA_RULES_SOURCE_NAME, apiVersion: 'legacy' },
|
{ dataSourceName: GRAFANA_RULES_SOURCE_NAME, apiVersion: 'legacy' },
|
||||||
'abcd',
|
grafanaRulerRule.grafana_alert.namespace_uid,
|
||||||
{
|
{
|
||||||
interval: '1m',
|
interval: '1m',
|
||||||
name: 'group1',
|
name: grafanaRulerGroup.name,
|
||||||
rules: [
|
rules: [
|
||||||
|
grafanaRulerRule,
|
||||||
{
|
{
|
||||||
annotations: { description: 'some description' },
|
annotations: { description: 'some description' },
|
||||||
labels: {},
|
labels: {},
|
||||||
|
@ -203,6 +203,13 @@ export const alertRuleApi = alertingApi.injectEndpoints({
|
|||||||
providesTags: ['CombinedAlertRule'],
|
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
|
// TODO This should be probably a separate ruler API file
|
||||||
rulerRuleGroup: build.query<
|
rulerRuleGroup: build.query<
|
||||||
RulerRuleGroupDTO,
|
RulerRuleGroupDTO,
|
||||||
@ -219,6 +226,7 @@ export const alertRuleApi = alertingApi.injectEndpoints({
|
|||||||
// TODO: In future, if supported in other rulers, parametrize ruler source name
|
// 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
|
// 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}` }),
|
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>({
|
exportRules: build.query<string, ExportRulesParams>({
|
||||||
|
@ -43,6 +43,7 @@ export const alertingApi = createApi({
|
|||||||
'DataSourceSettings',
|
'DataSourceSettings',
|
||||||
'GrafanaLabels',
|
'GrafanaLabels',
|
||||||
'CombinedAlertRule',
|
'CombinedAlertRule',
|
||||||
|
'GrafanaRulerRule',
|
||||||
],
|
],
|
||||||
endpoints: () => ({}),
|
endpoints: () => ({}),
|
||||||
});
|
});
|
||||||
|
@ -5,10 +5,10 @@ import { render, waitFor, waitForElementToBeRemoved, userEvent } from 'test/test
|
|||||||
import { byRole, byTestId, byText } from 'testing-library-selector';
|
import { byRole, byTestId, byText } from 'testing-library-selector';
|
||||||
|
|
||||||
import { DashboardSearchItemType } from '../../../../search/types';
|
import { DashboardSearchItemType } from '../../../../search/types';
|
||||||
import { mockAlertRuleApi, mockExportApi, mockSearchApi, setupMswServer } from '../../mockApi';
|
import { mockExportApi, mockSearchApi, setupMswServer } from '../../mockApi';
|
||||||
import { getGrafanaRule, mockDashboardSearchItem, mockDataSource } from '../../mocks';
|
import { mockDashboardSearchItem, mockDataSource } from '../../mocks';
|
||||||
|
import { grafanaRulerRule } from '../../mocks/alertRuleApi';
|
||||||
import { setupDataSources } from '../../testSetup/datasources';
|
import { setupDataSources } from '../../testSetup/datasources';
|
||||||
import { GRAFANA_RULES_SOURCE_NAME } from '../../utils/datasource';
|
|
||||||
|
|
||||||
import GrafanaModifyExport from './GrafanaModifyExport';
|
import GrafanaModifyExport from './GrafanaModifyExport';
|
||||||
|
|
||||||
@ -31,7 +31,7 @@ jest.mock('@grafana/ui', () => ({
|
|||||||
}));
|
}));
|
||||||
|
|
||||||
const ui = {
|
const ui = {
|
||||||
loading: byText('Loading the rule'),
|
loading: byText('Loading the rule...'),
|
||||||
form: {
|
form: {
|
||||||
nameInput: byRole('textbox', { name: 'name' }),
|
nameInput: byRole('textbox', { name: 'name' }),
|
||||||
folder: byTestId('folder-picker'),
|
folder: byTestId('folder-picker'),
|
||||||
@ -66,55 +66,27 @@ const server = setupMswServer();
|
|||||||
describe('GrafanaModifyExport', () => {
|
describe('GrafanaModifyExport', () => {
|
||||||
setupDataSources(dataSources.default);
|
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 () => {
|
it('Should render edit form for the specified rule', async () => {
|
||||||
mockSearchApi(server).search([
|
mockSearchApi(server).search([
|
||||||
mockDashboardSearchItem({
|
mockDashboardSearchItem({
|
||||||
title: grafanaRule.namespace.name,
|
title: grafanaRulerRule.grafana_alert.title,
|
||||||
uid: 'folderUID1',
|
uid: grafanaRulerRule.grafana_alert.namespace_uid,
|
||||||
url: '',
|
url: '',
|
||||||
tags: [],
|
tags: [],
|
||||||
type: DashboardSearchItemType.DashFolder,
|
type: DashboardSearchItemType.DashFolder,
|
||||||
}),
|
}),
|
||||||
]);
|
]);
|
||||||
mockAlertRuleApi(server).rulerRules(GRAFANA_RULES_SOURCE_NAME, {
|
mockExportApi(server).modifiedExport(grafanaRulerRule.grafana_alert.namespace_uid, {
|
||||||
[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', {
|
|
||||||
yaml: 'Yaml Export Content',
|
yaml: 'Yaml Export Content',
|
||||||
json: 'Json Export Content',
|
json: 'Json Export Content',
|
||||||
});
|
});
|
||||||
|
|
||||||
const user = userEvent.setup();
|
const user = userEvent.setup();
|
||||||
|
|
||||||
renderModifyExport('test-rule-uid');
|
renderModifyExport(grafanaRulerRule.grafana_alert.uid);
|
||||||
|
|
||||||
await waitForElementToBeRemoved(() => ui.loading.get());
|
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());
|
await user.click(ui.exportButton.get());
|
||||||
|
|
||||||
|
@ -1,14 +1,13 @@
|
|||||||
import * as React from 'react';
|
import * as React from 'react';
|
||||||
import { useEffect, useState } from 'react';
|
import { useMemo } from 'react';
|
||||||
import { useAsync } from 'react-use';
|
|
||||||
|
|
||||||
import { locationService } from '@grafana/runtime';
|
import { locationService } from '@grafana/runtime';
|
||||||
import { Alert, LoadingPlaceholder } from '@grafana/ui';
|
import { Alert, LoadingPlaceholder } from '@grafana/ui';
|
||||||
|
|
||||||
import { GrafanaRouteComponentProps } from '../../../../../core/navigation/types';
|
import { GrafanaRouteComponentProps } from '../../../../../core/navigation/types';
|
||||||
import { useDispatch } from '../../../../../types';
|
|
||||||
import { RuleIdentifier } from '../../../../../types/unified-alerting';
|
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 { formValuesFromExistingRule } from '../../utils/rule-form';
|
||||||
import * as ruleId from '../../utils/rule-id';
|
import * as ruleId from '../../utils/rule-id';
|
||||||
import { isGrafanaRulerRule } from '../../utils/rules';
|
import { isGrafanaRulerRule } from '../../utils/rules';
|
||||||
@ -19,79 +18,34 @@ import { ModifyExportRuleForm } from '../rule-editor/alert-rule-form/ModifyExpor
|
|||||||
interface GrafanaModifyExportProps extends GrafanaRouteComponentProps<{ id?: string }> {}
|
interface GrafanaModifyExportProps extends GrafanaRouteComponentProps<{ id?: string }> {}
|
||||||
|
|
||||||
export default function GrafanaModifyExport({ match }: GrafanaModifyExportProps) {
|
export default function GrafanaModifyExport({ match }: GrafanaModifyExportProps) {
|
||||||
const dispatch = useDispatch();
|
const ruleIdentifier = useMemo<RuleIdentifier | undefined>(() => {
|
||||||
|
return ruleId.tryParse(match.params.id, true);
|
||||||
// Get rule source build info
|
|
||||||
const [ruleIdentifier, setRuleIdentifier] = useState<RuleIdentifier | undefined>(undefined);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
const identifier = ruleId.tryParse(match.params.id, true);
|
|
||||||
setRuleIdentifier(identifier);
|
|
||||||
}, [match.params.id]);
|
}, [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) {
|
if (!ruleIdentifier) {
|
||||||
return <div>Rule not found</div>;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (loading) {
|
|
||||||
return <LoadingPlaceholder text="Loading the rule" />;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (error) {
|
|
||||||
return (
|
return (
|
||||||
<Alert title="Cannot load modify export" severity="error">
|
<ModifyExportWrapper>
|
||||||
{error.message}
|
<Alert title="Invalid rule ID" severity="error">
|
||||||
</Alert>
|
The rule UID in the page URL is invalid. Please check the URL and try again.
|
||||||
);
|
</Alert>
|
||||||
}
|
</ModifyExportWrapper>
|
||||||
|
|
||||||
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>
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<ModifyExportWrapper>
|
||||||
|
<RuleModifyExport ruleIdentifier={ruleIdentifier} />
|
||||||
|
</ModifyExportWrapper>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
interface ModifyExportWrapperProps {
|
||||||
|
children: React.ReactNode;
|
||||||
|
}
|
||||||
|
|
||||||
|
function ModifyExportWrapper({ children }: ModifyExportWrapperProps) {
|
||||||
return (
|
return (
|
||||||
<AlertingPageWrapper
|
<AlertingPageWrapper
|
||||||
isLoading={loading}
|
|
||||||
navId="alert-list"
|
navId="alert-list"
|
||||||
pageNav={{
|
pageNav={{
|
||||||
text: 'Modify export',
|
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.',
|
'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 && (
|
{children}
|
||||||
<ModifyExportRuleForm ruleForm={formValuesFromExistingRule(alertRule)} alertUid={match.params.id ?? ''} />
|
|
||||||
)}
|
|
||||||
</AlertingPageWrapper>
|
</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 { css } from '@emotion/css';
|
||||||
import { debounce, take, uniqueId } from 'lodash';
|
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 { FormProvider, useForm, useFormContext, Controller } from 'react-hook-form';
|
||||||
|
|
||||||
import { AppEvents, GrafanaTheme2, SelectableValue } from '@grafana/data';
|
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 appEvents from 'app/core/app_events';
|
||||||
import { contextSrv } from 'app/core/services/context_srv';
|
import { contextSrv } from 'app/core/services/context_srv';
|
||||||
import { createFolder } from 'app/features/manage-dashboards/state/actions';
|
import { createFolder } from 'app/features/manage-dashboards/state/actions';
|
||||||
import { AccessControlAction, useDispatch } from 'app/types';
|
import { AccessControlAction } from 'app/types';
|
||||||
import { CombinedRuleGroup } from 'app/types/unified-alerting';
|
import { RulerRuleGroupDTO, RulerRulesConfigDTO } from 'app/types/unified-alerting-dto';
|
||||||
import { RulerRulesConfigDTO } from 'app/types/unified-alerting-dto';
|
|
||||||
|
|
||||||
import { useCombinedRuleNamespaces } from '../../hooks/useCombinedRuleNamespaces';
|
import { alertRuleApi } from '../../api/alertRuleApi';
|
||||||
import { useUnifiedAlertingSelector } from '../../hooks/useUnifiedAlertingSelector';
|
import { grafanaRulerConfig } from '../../hooks/useCombinedRule';
|
||||||
import { fetchRulerRulesAction } from '../../state/actions';
|
|
||||||
import { RuleFormValues } from '../../types/rule-form';
|
import { RuleFormValues } from '../../types/rule-form';
|
||||||
import { GRAFANA_RULES_SOURCE_NAME } from '../../utils/datasource';
|
|
||||||
import { DEFAULT_GROUP_EVALUATION_INTERVAL } from '../../utils/rule-form';
|
import { DEFAULT_GROUP_EVALUATION_INTERVAL } from '../../utils/rule-form';
|
||||||
import { isGrafanaRulerRule } from '../../utils/rules';
|
import { isGrafanaRulerRule } from '../../utils/rules';
|
||||||
import { ProvisioningBadge } from '../Provisioning';
|
import { ProvisioningBadge } from '../Provisioning';
|
||||||
@ -30,42 +27,51 @@ import { checkForPathSeparator } from './util';
|
|||||||
export const MAX_GROUP_RESULTS = 1000;
|
export const MAX_GROUP_RESULTS = 1000;
|
||||||
|
|
||||||
export const useFolderGroupOptions = (folderUid: string, enableProvisionedGroups: boolean) => {
|
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
|
// fetch the ruler rules from the database so we can figure out what other "groups" are already defined
|
||||||
// for our folders
|
// for our folders
|
||||||
useEffect(() => {
|
const { isLoading: isLoadingRulerNamespace, currentData: rulerNamespace } =
|
||||||
dispatch(fetchRulerRulesAction({ rulesSourceName: GRAFANA_RULES_SOURCE_NAME }));
|
alertRuleApi.endpoints.rulerNamespace.useQuery(
|
||||||
}, [dispatch]);
|
{
|
||||||
|
namespace: folderUid,
|
||||||
|
rulerConfig: grafanaRulerConfig,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
skip: !folderUid,
|
||||||
|
refetchOnMountOrArgChange: true,
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
const rulerRuleRequests = useUnifiedAlertingSelector((state) => state.rulerRules);
|
// There should be only one entry in the rulerNamespace object
|
||||||
const groupfoldersForGrafana = rulerRuleRequests[GRAFANA_RULES_SOURCE_NAME];
|
// 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 = Object.values(rulerNamespace).flat() ?? [];
|
||||||
const folderGroups = grafanaFolders.find((f) => f.uid === folderUid)?.groups ?? [];
|
|
||||||
|
|
||||||
const groupOptions = folderGroups
|
return folderGroups
|
||||||
.map<SelectableValue<string>>((group) => {
|
.map<SelectableValue<string>>((group) => {
|
||||||
const isProvisioned = isProvisionedGroup(group);
|
const isProvisioned = isProvisionedGroup(group);
|
||||||
return {
|
return {
|
||||||
label: group.name,
|
label: group.name,
|
||||||
value: group.name,
|
value: group.name,
|
||||||
description: group.interval ?? DEFAULT_GROUP_EVALUATION_INTERVAL,
|
description: group.interval ?? DEFAULT_GROUP_EVALUATION_INTERVAL,
|
||||||
// we include provisioned folders, but disable the option to select them
|
// we include provisioned folders, but disable the option to select them
|
||||||
isDisabled: !enableProvisionedGroups ? isProvisioned : false,
|
isDisabled: !enableProvisionedGroups ? isProvisioned : false,
|
||||||
isProvisioned: isProvisioned,
|
isProvisioned: isProvisioned,
|
||||||
};
|
};
|
||||||
})
|
})
|
||||||
|
|
||||||
.sort(sortByLabel);
|
.sort(sortByLabel);
|
||||||
|
}, [rulerNamespace, enableProvisionedGroups]);
|
||||||
|
|
||||||
return { groupOptions, loading: groupfoldersForGrafana?.loading };
|
return { groupOptions, loading: isLoadingRulerNamespace };
|
||||||
};
|
};
|
||||||
|
|
||||||
const isProvisionedGroup = (group: CombinedRuleGroup) => {
|
const isProvisionedGroup = (group: RulerRuleGroupDTO) => {
|
||||||
return group.rules.some(
|
return group.rules.some((rule) => isGrafanaRulerRule(rule) && Boolean(rule.grafana_alert.provenance) === true);
|
||||||
(rule) => isGrafanaRulerRule(rule.rulerRule) && Boolean(rule.rulerRule.grafana_alert.provenance) === true
|
|
||||||
);
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const sortByLabel = (a: SelectableValue<string>, b: SelectableValue<string>) => {
|
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 { clickSelectOption } from 'test/helpers/selectOptionInTest';
|
||||||
import { byRole } from 'testing-library-selector';
|
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 { contextSrv } from 'app/core/services/context_srv';
|
||||||
import RuleEditor from 'app/features/alerting/unified/RuleEditor';
|
import RuleEditor from 'app/features/alerting/unified/RuleEditor';
|
||||||
import { discoverFeatures } from 'app/features/alerting/unified/api/buildInfo';
|
import * as ruler from 'app/features/alerting/unified/api/ruler';
|
||||||
import {
|
|
||||||
fetchRulerRules,
|
|
||||||
fetchRulerRulesGroup,
|
|
||||||
fetchRulerRulesNamespace,
|
|
||||||
setRulerRuleGroup,
|
|
||||||
} from 'app/features/alerting/unified/api/ruler';
|
|
||||||
import * as useContactPoints from 'app/features/alerting/unified/components/contact-points/useContactPoints';
|
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 { 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 { 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 * as utils_config from 'app/features/alerting/unified/utils/config';
|
||||||
import {
|
import {
|
||||||
AlertManagerDataSource,
|
|
||||||
DataSourceType,
|
DataSourceType,
|
||||||
GRAFANA_DATASOURCE_NAME,
|
GRAFANA_DATASOURCE_NAME,
|
||||||
GRAFANA_RULES_SOURCE_NAME,
|
GRAFANA_RULES_SOURCE_NAME,
|
||||||
getAlertManagerDataSourcesByPermission,
|
|
||||||
useGetAlertManagerDataSourcesByPermissionAndConfig,
|
useGetAlertManagerDataSourcesByPermissionAndConfig,
|
||||||
} from 'app/features/alerting/unified/utils/datasource';
|
} from 'app/features/alerting/unified/utils/datasource';
|
||||||
import { getDefaultQueries } from 'app/features/alerting/unified/utils/rule-form';
|
import { getDefaultQueries } from 'app/features/alerting/unified/utils/rule-form';
|
||||||
import { searchFolders } from 'app/features/manage-dashboards/state/actions';
|
import { searchFolders } from 'app/features/manage-dashboards/state/actions';
|
||||||
import { DashboardSearchHit, DashboardSearchItemType } from 'app/features/search/types';
|
import { DashboardSearchHit, DashboardSearchItemType } from 'app/features/search/types';
|
||||||
import { AccessControlAction } from 'app/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 { RECEIVER_META_KEY } from '../../../contact-points/useContactPoints';
|
||||||
import { ContactPointWithMetadata } from '../../../contact-points/utils';
|
import { ContactPointWithMetadata } from '../../../contact-points/utils';
|
||||||
import { ExpressionEditorProps } from '../../ExpressionEditor';
|
import { ExpressionEditorProps } from '../../ExpressionEditor';
|
||||||
|
|
||||||
jest.mock('app/features/alerting/unified/components/rule-editor/ExpressionEditor', () => ({
|
jest.mock('app/features/alerting/unified/components/rule-editor/ExpressionEditor', () => ({
|
||||||
// eslint-disable-next-line react/display-name
|
|
||||||
ExpressionEditor: ({ value, onChange }: ExpressionEditorProps) => (
|
ExpressionEditor: ({ value, onChange }: ExpressionEditorProps) => (
|
||||||
<input value={value} data-testid="expr" onChange={(e) => onChange(e.target.value)} />
|
<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/features/manage-dashboards/state/actions');
|
||||||
|
|
||||||
jest.mock('app/core/components/AppChrome/AppChromeUpdate', () => ({
|
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.
|
// there's no angular scope in test and things go terribly wrong when trying to render the query editor row.
|
||||||
// lets just skip it
|
// lets just skip it
|
||||||
jest.mock('app/features/query/components/QueryEditorRow', () => ({
|
jest.mock('app/features/query/components/QueryEditorRow', () => ({
|
||||||
// eslint-disable-next-line react/display-name
|
|
||||||
QueryEditorRow: () => <p>hi</p>,
|
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();
|
const user = userEvent.setup();
|
||||||
|
|
||||||
jest.spyOn(utils_config, 'getAllDataSources');
|
// jest.spyOn(utils_config, 'getAllDataSources');
|
||||||
jest.spyOn(dsByPermission, 'useAlertManagersByPermission');
|
// jest.spyOn(dsByPermission, 'useAlertManagersByPermission');
|
||||||
jest.spyOn(useContactPoints, 'useContactPointsWithStatus');
|
jest.spyOn(useContactPoints, 'useContactPointsWithStatus');
|
||||||
|
|
||||||
jest.setTimeout(60 * 1000);
|
jest.setTimeout(60 * 1000);
|
||||||
@ -92,14 +65,8 @@ const mocks = {
|
|||||||
searchFolders: jest.mocked(searchFolders),
|
searchFolders: jest.mocked(searchFolders),
|
||||||
useContactPointsWithStatus: jest.mocked(useContactPoints.useContactPointsWithStatus),
|
useContactPointsWithStatus: jest.mocked(useContactPoints.useContactPointsWithStatus),
|
||||||
useGetAlertManagerDataSourcesByPermissionAndConfig: jest.mocked(useGetAlertManagerDataSourcesByPermissionAndConfig),
|
useGetAlertManagerDataSourcesByPermissionAndConfig: jest.mocked(useGetAlertManagerDataSourcesByPermissionAndConfig),
|
||||||
getAlertManagerDataSourcesByPermission: jest.mocked(getAlertManagerDataSourcesByPermission),
|
|
||||||
api: {
|
api: {
|
||||||
discoverFeatures: jest.mocked(discoverFeatures),
|
setRulerRuleGroup: jest.spyOn(ruler, 'setRulerRuleGroup'),
|
||||||
fetchRulerRulesGroup: jest.mocked(fetchRulerRulesGroup),
|
|
||||||
setRulerRuleGroup: jest.mocked(setRulerRuleGroup),
|
|
||||||
fetchRulerRulesNamespace: jest.mocked(fetchRulerRulesNamespace),
|
|
||||||
fetchRulerRules: jest.mocked(fetchRulerRules),
|
|
||||||
fetchRulerRulesIfNotFetchedYet: jest.mocked(fetchRulerRulesIfNotFetchedYet),
|
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -125,17 +92,6 @@ describe('Can create a new grafana managed alert unsing simplified routing', ()
|
|||||||
AccessControlAction.AlertingNotificationsRead,
|
AccessControlAction.AlertingNotificationsRead,
|
||||||
AccessControlAction.AlertingNotificationsWrite,
|
AccessControlAction.AlertingNotificationsWrite,
|
||||||
]);
|
]);
|
||||||
mocks.getAlertManagerDataSourcesByPermission.mockReturnValue({
|
|
||||||
availableInternalDataSources: [grafanaAlertManagerDataSource],
|
|
||||||
availableExternalDataSources: [],
|
|
||||||
});
|
|
||||||
|
|
||||||
mocks.useGetAlertManagerDataSourcesByPermissionAndConfig.mockReturnValue([grafanaAlertManagerDataSource]);
|
|
||||||
|
|
||||||
jest.mocked(dsByPermission.useAlertManagersByPermission).mockReturnValue({
|
|
||||||
availableInternalDataSources: [grafanaAlertManagerDataSource],
|
|
||||||
availableExternalDataSources: [],
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
|
|
||||||
const dataSources = {
|
const dataSources = {
|
||||||
@ -152,6 +108,7 @@ describe('Can create a new grafana managed alert unsing simplified routing', ()
|
|||||||
type: DataSourceType.Alertmanager,
|
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 () => {
|
it('cannot create new grafana managed alert when using simplified routing and not selecting a contact point', async () => {
|
||||||
// no contact points found
|
// no contact points found
|
||||||
@ -162,64 +119,11 @@ describe('Can create a new grafana managed alert unsing simplified routing', ()
|
|||||||
refetchReceivers: jest.fn(),
|
refetchReceivers: jest.fn(),
|
||||||
});
|
});
|
||||||
|
|
||||||
setDataSourceSrv(new MockDataSourceSrv(dataSources));
|
|
||||||
mocks.getAllDataSources.mockReturnValue(Object.values(dataSources));
|
|
||||||
mocks.api.setRulerRuleGroup.mockResolvedValue();
|
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([
|
mocks.searchFolders.mockResolvedValue([
|
||||||
{
|
{
|
||||||
title: 'Folder A',
|
title: 'Folder A',
|
||||||
uid: 'abcd',
|
uid: grafanaRulerRule.grafana_alert.namespace_uid,
|
||||||
id: 1,
|
id: 1,
|
||||||
type: DashboardSearchItemType.DashDB,
|
type: DashboardSearchItemType.DashDB,
|
||||||
},
|
},
|
||||||
@ -235,12 +139,6 @@ describe('Can create a new grafana managed alert unsing simplified routing', ()
|
|||||||
},
|
},
|
||||||
] as DashboardSearchHit[]);
|
] as DashboardSearchHit[]);
|
||||||
|
|
||||||
mocks.api.discoverFeatures.mockResolvedValue({
|
|
||||||
application: PromApplication.Prometheus,
|
|
||||||
features: {
|
|
||||||
rulerApiEnabled: false,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
config.featureToggles.alertingSimplifiedRouting = true;
|
config.featureToggles.alertingSimplifiedRouting = true;
|
||||||
renderSimplifiedRuleEditor();
|
renderSimplifiedRuleEditor();
|
||||||
await waitForElementToBeRemoved(screen.getAllByTestId('Spinner'));
|
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');
|
await clickSelectOption(folderInput, 'Folder A');
|
||||||
const groupInput = await ui.inputs.group.find();
|
const groupInput = await ui.inputs.group.find();
|
||||||
await user.click(byRole('combobox').get(groupInput));
|
await user.click(byRole('combobox').get(groupInput));
|
||||||
await clickSelectOption(groupInput, 'group1');
|
await clickSelectOption(groupInput, grafanaRulerRule.grafana_alert.rule_group);
|
||||||
//select contact point routing
|
//select contact point routing
|
||||||
await user.click(ui.inputs.simplifiedRouting.contactPointRouting.get());
|
await user.click(ui.inputs.simplifiedRouting.contactPointRouting.get());
|
||||||
// do not select a contact point
|
// do not select a contact point
|
||||||
@ -289,64 +187,11 @@ describe('Can create a new grafana managed alert unsing simplified routing', ()
|
|||||||
refetchReceivers: jest.fn(),
|
refetchReceivers: jest.fn(),
|
||||||
});
|
});
|
||||||
|
|
||||||
setDataSourceSrv(new MockDataSourceSrv(dataSources));
|
|
||||||
mocks.getAllDataSources.mockReturnValue(Object.values(dataSources));
|
|
||||||
mocks.api.setRulerRuleGroup.mockResolvedValue();
|
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([
|
mocks.searchFolders.mockResolvedValue([
|
||||||
{
|
{
|
||||||
title: 'Folder A',
|
title: 'Folder A',
|
||||||
uid: 'abcd',
|
uid: grafanaRulerNamespace2.uid,
|
||||||
id: 1,
|
id: 1,
|
||||||
type: DashboardSearchItemType.DashDB,
|
type: DashboardSearchItemType.DashDB,
|
||||||
},
|
},
|
||||||
@ -364,12 +209,6 @@ describe('Can create a new grafana managed alert unsing simplified routing', ()
|
|||||||
},
|
},
|
||||||
] as DashboardSearchHit[]);
|
] as DashboardSearchHit[]);
|
||||||
|
|
||||||
mocks.api.discoverFeatures.mockResolvedValue({
|
|
||||||
application: PromApplication.Prometheus,
|
|
||||||
features: {
|
|
||||||
rulerApiEnabled: false,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
config.featureToggles.alertingSimplifiedRouting = true;
|
config.featureToggles.alertingSimplifiedRouting = true;
|
||||||
renderSimplifiedRuleEditor();
|
renderSimplifiedRuleEditor();
|
||||||
await waitForElementToBeRemoved(screen.getAllByTestId('Spinner'));
|
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');
|
await clickSelectOption(folderInput, 'Folder A');
|
||||||
const groupInput = await ui.inputs.group.find();
|
const groupInput = await ui.inputs.group.find();
|
||||||
await user.click(byRole('combobox').get(groupInput));
|
await user.click(byRole('combobox').get(groupInput));
|
||||||
await clickSelectOption(groupInput, 'group1');
|
await clickSelectOption(groupInput, grafanaRulerEmptyGroup.name);
|
||||||
//select contact point routing
|
//select contact point routing
|
||||||
await user.click(ui.inputs.simplifiedRouting.contactPointRouting.get());
|
await user.click(ui.inputs.simplifiedRouting.contactPointRouting.get());
|
||||||
const contactPointInput = await ui.inputs.simplifiedRouting.contactPoint.find();
|
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());
|
await waitFor(() => expect(mocks.api.setRulerRuleGroup).toHaveBeenCalled());
|
||||||
expect(mocks.api.setRulerRuleGroup).toHaveBeenCalledWith(
|
expect(mocks.api.setRulerRuleGroup).toHaveBeenCalledWith(
|
||||||
{ dataSourceName: GRAFANA_RULES_SOURCE_NAME, apiVersion: 'legacy' },
|
{ dataSourceName: GRAFANA_RULES_SOURCE_NAME, apiVersion: 'legacy' },
|
||||||
'abcd',
|
grafanaRulerNamespace2.uid,
|
||||||
{
|
{
|
||||||
interval: '1m',
|
interval: grafanaRulerEmptyGroup.interval,
|
||||||
name: 'group1',
|
name: grafanaRulerEmptyGroup.name,
|
||||||
rules: [
|
rules: [
|
||||||
{
|
{
|
||||||
annotations: {},
|
annotations: {},
|
||||||
|
@ -1,24 +1,24 @@
|
|||||||
|
import { within } from '@testing-library/react';
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { render, waitFor, screen, userEvent } from 'test/test-utils';
|
import { render, waitFor, screen, userEvent } from 'test/test-utils';
|
||||||
import { byText, byRole } from 'testing-library-selector';
|
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 { backendSrv } from 'app/core/services/backend_srv';
|
||||||
import { setupMswServer } from 'app/features/alerting/unified/mockApi';
|
import { setupMswServer } from 'app/features/alerting/unified/mockApi';
|
||||||
import { setFolderAccessControl } from 'app/features/alerting/unified/mocks/server/configure';
|
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 { AlertManagerDataSourceJsonData } from 'app/plugins/datasource/alertmanager/types';
|
||||||
import { AccessControlAction } from 'app/types';
|
import { AccessControlAction } from 'app/types';
|
||||||
import { CombinedRule, RuleIdentifier } from 'app/types/unified-alerting';
|
import { CombinedRule, RuleIdentifier } from 'app/types/unified-alerting';
|
||||||
|
|
||||||
import {
|
import {
|
||||||
MockDataSourceSrv,
|
|
||||||
getCloudRule,
|
getCloudRule,
|
||||||
getGrafanaRule,
|
getGrafanaRule,
|
||||||
grantUserPermissions,
|
grantUserPermissions,
|
||||||
mockDataSource,
|
mockDataSource,
|
||||||
mockPluginLinkExtension,
|
mockPluginLinkExtension,
|
||||||
} from '../../mocks';
|
} from '../../mocks';
|
||||||
|
import { grafanaRulerRule } from '../../mocks/alertRuleApi';
|
||||||
import { setupDataSources } from '../../testSetup/datasources';
|
import { setupDataSources } from '../../testSetup/datasources';
|
||||||
import { Annotation } from '../../utils/constants';
|
import { Annotation } from '../../utils/constants';
|
||||||
import { DataSourceType, GRAFANA_RULES_SOURCE_NAME } from '../../utils/datasource';
|
import { DataSourceType, GRAFANA_RULES_SOURCE_NAME } from '../../utils/datasource';
|
||||||
@ -104,7 +104,7 @@ describe('RuleViewer', () => {
|
|||||||
totals: { alerting: 1 },
|
totals: { alerting: 1 },
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{ uid: 'test1' }
|
{ uid: grafanaRulerRule.grafana_alert.uid }
|
||||||
);
|
);
|
||||||
const mockRuleIdentifier = ruleId.fromCombinedRule('grafana', mockRule);
|
const mockRuleIdentifier = ruleId.fromCombinedRule('grafana', mockRule);
|
||||||
|
|
||||||
@ -114,6 +114,7 @@ describe('RuleViewer', () => {
|
|||||||
AccessControlAction.AlertingRuleRead,
|
AccessControlAction.AlertingRuleRead,
|
||||||
AccessControlAction.AlertingRuleUpdate,
|
AccessControlAction.AlertingRuleUpdate,
|
||||||
AccessControlAction.AlertingRuleDelete,
|
AccessControlAction.AlertingRuleDelete,
|
||||||
|
AccessControlAction.AlertingInstanceRead,
|
||||||
AccessControlAction.AlertingInstanceCreate,
|
AccessControlAction.AlertingInstanceCreate,
|
||||||
]);
|
]);
|
||||||
setBackendSrv(backendSrv);
|
setBackendSrv(backendSrv);
|
||||||
@ -181,7 +182,6 @@ describe('RuleViewer', () => {
|
|||||||
}),
|
}),
|
||||||
};
|
};
|
||||||
setupDataSources(dataSources.grafana, dataSources.am);
|
setupDataSources(dataSources.grafana, dataSources.am);
|
||||||
setDataSourceSrv(new MockDataSourceSrv(dataSources));
|
|
||||||
|
|
||||||
await renderRuleViewer(mockRule, mockRuleIdentifier);
|
await renderRuleViewer(mockRule, mockRuleIdentifier);
|
||||||
|
|
||||||
@ -189,7 +189,10 @@ describe('RuleViewer', () => {
|
|||||||
await user.click(ELEMENTS.actions.more.button.get());
|
await user.click(ELEMENTS.actions.more.button.get());
|
||||||
await user.click(ELEMENTS.actions.more.actions.silence.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 { useAsync } from 'react-use';
|
||||||
|
|
||||||
import { useDispatch } from 'app/types';
|
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 { RulerRuleGroupDTO, RulerRulesConfigDTO } from 'app/types/unified-alerting-dto';
|
||||||
|
|
||||||
import { alertRuleApi } from '../api/alertRuleApi';
|
import { alertRuleApi } from '../api/alertRuleApi';
|
||||||
@ -18,11 +25,7 @@ import {
|
|||||||
isRulerNotSupportedResponse,
|
isRulerNotSupportedResponse,
|
||||||
} from '../utils/rules';
|
} from '../utils/rules';
|
||||||
|
|
||||||
import {
|
import { attachRulerRulesToCombinedRules, useCombinedRuleNamespaces } from './useCombinedRuleNamespaces';
|
||||||
attachRulerRulesToCombinedRules,
|
|
||||||
combineRulesNamespaces,
|
|
||||||
useCombinedRuleNamespaces,
|
|
||||||
} from './useCombinedRuleNamespaces';
|
|
||||||
import { useUnifiedAlertingSelector } from './useUnifiedAlertingSelector';
|
import { useUnifiedAlertingSelector } from './useUnifiedAlertingSelector';
|
||||||
|
|
||||||
export function useCombinedRulesMatching(
|
export function useCombinedRulesMatching(
|
||||||
@ -162,15 +165,25 @@ function getRequestState(
|
|||||||
return state;
|
return state;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function useCombinedRule({ ruleIdentifier }: { ruleIdentifier: RuleIdentifier }): {
|
interface RequestState<T> {
|
||||||
|
result?: T;
|
||||||
loading: boolean;
|
loading: boolean;
|
||||||
result?: CombinedRule;
|
|
||||||
error?: unknown;
|
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 { ruleSourceName } = ruleIdentifier;
|
||||||
const dsSettings = getDataSourceByName(ruleSourceName);
|
const ruleSource = getRulesSourceFromIdentifier(ruleIdentifier);
|
||||||
|
|
||||||
const { dsFeatures, isLoadingDsFeatures } = useDataSourceFeatures(ruleSourceName);
|
const { dsFeatures, isLoadingDsFeatures } = useDataSourceFeatures(ruleSourceName);
|
||||||
|
const {
|
||||||
|
loading: isLoadingRuleLocation,
|
||||||
|
error: ruleLocationError,
|
||||||
|
result: ruleLocation,
|
||||||
|
} = useRuleLocation(ruleIdentifier);
|
||||||
|
|
||||||
const {
|
const {
|
||||||
currentData: promRuleNs,
|
currentData: promRuleNs,
|
||||||
@ -178,85 +191,47 @@ export function useCombinedRule({ ruleIdentifier }: { ruleIdentifier: RuleIdenti
|
|||||||
error: promRuleNsError,
|
error: promRuleNsError,
|
||||||
} = alertRuleApi.endpoints.prometheusRuleNamespaces.useQuery(
|
} = alertRuleApi.endpoints.prometheusRuleNamespaces.useQuery(
|
||||||
{
|
{
|
||||||
// TODO Refactor parameters
|
|
||||||
ruleSourceName: ruleIdentifier.ruleSourceName,
|
ruleSourceName: ruleIdentifier.ruleSourceName,
|
||||||
namespace:
|
namespace: ruleLocation?.namespace,
|
||||||
isPrometheusRuleIdentifier(ruleIdentifier) || isCloudRuleIdentifier(ruleIdentifier)
|
groupName: ruleLocation?.group,
|
||||||
? ruleIdentifier.namespace
|
ruleName: ruleLocation?.ruleName,
|
||||||
: undefined,
|
},
|
||||||
groupName:
|
{
|
||||||
isPrometheusRuleIdentifier(ruleIdentifier) || isCloudRuleIdentifier(ruleIdentifier)
|
skip: !ruleLocation || isLoadingRuleLocation,
|
||||||
? ruleIdentifier.groupName
|
refetchOnMountOrArgChange: true,
|
||||||
: undefined,
|
|
||||||
ruleName:
|
|
||||||
isPrometheusRuleIdentifier(ruleIdentifier) || isCloudRuleIdentifier(ruleIdentifier)
|
|
||||||
? ruleIdentifier.ruleName
|
|
||||||
: undefined,
|
|
||||||
}
|
}
|
||||||
// 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 [
|
const [
|
||||||
fetchRulerRuleGroup,
|
fetchRulerRuleGroup,
|
||||||
{ currentData: rulerRuleGroup, isLoading: isLoadingRulerGroup, error: rulerRuleGroupError },
|
{
|
||||||
|
currentData: rulerRuleGroup,
|
||||||
|
isLoading: isLoadingRulerGroup,
|
||||||
|
error: rulerRuleGroupError,
|
||||||
|
isUninitialized: rulerRuleGroupUninitialized,
|
||||||
|
},
|
||||||
] = alertRuleApi.endpoints.rulerRuleGroup.useLazyQuery();
|
] = alertRuleApi.endpoints.rulerRuleGroup.useLazyQuery();
|
||||||
|
|
||||||
const [fetchRulerRules, { currentData: rulerRules, isLoading: isLoadingRulerRules, error: rulerRulesError }] =
|
|
||||||
alertRuleApi.endpoints.rulerRules.useLazyQuery();
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!dsFeatures?.rulerConfig) {
|
if (!dsFeatures?.rulerConfig || !ruleLocation) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (dsFeatures.rulerConfig && isCloudRuleIdentifier(ruleIdentifier)) {
|
fetchRulerRuleGroup({
|
||||||
fetchRulerRuleGroup({
|
rulerConfig: dsFeatures.rulerConfig,
|
||||||
rulerConfig: dsFeatures.rulerConfig,
|
namespace: ruleLocation.namespace,
|
||||||
namespace: ruleIdentifier.namespace,
|
group: ruleLocation.group,
|
||||||
group: ruleIdentifier.groupName,
|
});
|
||||||
});
|
}, [dsFeatures, fetchRulerRuleGroup, ruleLocation]);
|
||||||
} 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]);
|
|
||||||
|
|
||||||
const rule = useMemo(() => {
|
const rule = useMemo(() => {
|
||||||
if (!promRuleNs) {
|
if (!promRuleNs || !ruleSource) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (isGrafanaRuleIdentifier(ruleIdentifier)) {
|
if (promRuleNs.length > 0) {
|
||||||
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))
|
|
||||||
) {
|
|
||||||
const namespaces = promRuleNs.map((ns) =>
|
const namespaces = promRuleNs.map((ns) =>
|
||||||
attachRulerRulesToCombinedRules(dsSettings, ns, rulerRuleGroup ? [rulerRuleGroup] : [])
|
attachRulerRulesToCombinedRules(ruleSource, ns, rulerRuleGroup ? [rulerRuleGroup] : [])
|
||||||
);
|
);
|
||||||
|
|
||||||
for (const namespace of namespaces) {
|
for (const namespace of namespaces) {
|
||||||
@ -273,15 +248,147 @@ export function useCombinedRule({ ruleIdentifier }: { ruleIdentifier: RuleIdenti
|
|||||||
}
|
}
|
||||||
|
|
||||||
return;
|
return;
|
||||||
}, [ruleIdentifier, ruleSourceName, promRuleNs, rulerRuleGroup, rulerRules, dsSettings]);
|
}, [ruleIdentifier, ruleSourceName, promRuleNs, rulerRuleGroup, ruleSource]);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
loading: isLoadingDsFeatures || isLoadingPromRules || isLoadingRulerGroup || isLoadingRulerRules,
|
loading: isLoadingDsFeatures || isLoadingPromRules || isLoadingRulerGroup || rulerRuleGroupUninitialized,
|
||||||
error: promRuleNsError ?? rulerRuleGroupError ?? rulerRulesError,
|
error: ruleLocationError ?? promRuleNsError ?? rulerRuleGroupError,
|
||||||
result: rule,
|
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 = {
|
export const grafanaRulerConfig: RulerDataSourceConfig = {
|
||||||
dataSourceName: GRAFANA_RULES_SOURCE_NAME,
|
dataSourceName: GRAFANA_RULES_SOURCE_NAME,
|
||||||
apiVersion: 'legacy',
|
apiVersion: 'legacy',
|
||||||
|
@ -117,6 +117,7 @@ export const mockRulerGrafanaRule = (
|
|||||||
uid: '123',
|
uid: '123',
|
||||||
title: 'myalert',
|
title: 'myalert',
|
||||||
namespace_uid: '123',
|
namespace_uid: '123',
|
||||||
|
rule_group: 'my-group',
|
||||||
condition: 'A',
|
condition: 'A',
|
||||||
no_data_state: GrafanaAlertStateDecision.Alerting,
|
no_data_state: GrafanaAlertStateDecision.Alerting,
|
||||||
exec_err_state: GrafanaAlertStateDecision.Alerting,
|
exec_err_state: GrafanaAlertStateDecision.Alerting,
|
||||||
@ -214,6 +215,7 @@ export const mockGrafanaRulerRule = (partial: Partial<GrafanaRuleDefinition> = {
|
|||||||
uid: '',
|
uid: '',
|
||||||
title: 'my rule',
|
title: 'my rule',
|
||||||
namespace_uid: 'NAMESPACE_UID',
|
namespace_uid: 'NAMESPACE_UID',
|
||||||
|
rule_group: 'my-group',
|
||||||
condition: '',
|
condition: '',
|
||||||
no_data_state: GrafanaAlertStateDecision.NoData,
|
no_data_state: GrafanaAlertStateDecision.NoData,
|
||||||
exec_err_state: GrafanaAlertStateDecision.Error,
|
exec_err_state: GrafanaAlertStateDecision.Error,
|
||||||
|
@ -1,9 +1,15 @@
|
|||||||
import { http, HttpResponse } from 'msw';
|
import { http, HttpResponse } from 'msw';
|
||||||
import { SetupServer } from 'msw/node';
|
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 { PreviewResponse, PREVIEW_URL, PROM_RULES_URL } from '../api/alertRuleApi';
|
||||||
|
import { Annotation } from '../utils/constants';
|
||||||
|
|
||||||
export function mockPreviewApiResponse(server: SetupServer, result: PreviewResponse) {
|
export function mockPreviewApiResponse(server: SetupServer, result: PreviewResponse) {
|
||||||
server.use(http.post(PREVIEW_URL, () => HttpResponse.json(result)));
|
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) {
|
export function mockPromRulesApiResponse(server: SetupServer, result: PromRulesResponse) {
|
||||||
server.use(http.get(PROM_RULES_URL, () => HttpResponse.json(result)));
|
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,
|
...folderHandlers,
|
||||||
...pluginsHandlers,
|
...pluginsHandlers,
|
||||||
...silenceHandlers,
|
...silenceHandlers,
|
||||||
|
...alertRuleHandlers,
|
||||||
];
|
];
|
||||||
|
|
||||||
export default allHandlers;
|
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';
|
export const MOCK_GRAFANA_ALERT_RULE_TITLE = 'Test alert';
|
||||||
|
|
||||||
const alertRuleDetailsHandler = () =>
|
import {
|
||||||
http.get<{ folderUid: string }>(`/api/ruler/:ruler/api/v1/rule/:uid`, () => {
|
RulerGrafanaRuleDTO,
|
||||||
// TODO: Scaffold out alert rule response logic as this endpoint is used more in tests
|
RulerRuleGroupDTO,
|
||||||
return HttpResponse.json({
|
RulerRulesConfigDTO,
|
||||||
grafana_alert: {
|
} from '../../../../../../types/unified-alerting-dto';
|
||||||
title: MOCK_GRAFANA_ALERT_RULE_TITLE,
|
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;
|
export default handlers;
|
||||||
|
@ -17,8 +17,8 @@ import {
|
|||||||
PromBasedDataSource,
|
PromBasedDataSource,
|
||||||
RuleIdentifier,
|
RuleIdentifier,
|
||||||
RuleNamespace,
|
RuleNamespace,
|
||||||
RulerDataSourceConfig,
|
|
||||||
RuleWithLocation,
|
RuleWithLocation,
|
||||||
|
RulerDataSourceConfig,
|
||||||
StateHistoryItem,
|
StateHistoryItem,
|
||||||
} from 'app/types/unified-alerting';
|
} from 'app/types/unified-alerting';
|
||||||
import {
|
import {
|
||||||
@ -30,15 +30,16 @@ import {
|
|||||||
|
|
||||||
import { backendSrv } from '../../../../core/services/backend_srv';
|
import { backendSrv } from '../../../../core/services/backend_srv';
|
||||||
import {
|
import {
|
||||||
|
LogMessages,
|
||||||
logError,
|
logError,
|
||||||
logInfo,
|
logInfo,
|
||||||
LogMessages,
|
|
||||||
trackSwitchToPoliciesRouting,
|
trackSwitchToPoliciesRouting,
|
||||||
trackSwitchToSimplifiedRouting,
|
trackSwitchToSimplifiedRouting,
|
||||||
withPerformanceLogging,
|
withPerformanceLogging,
|
||||||
withPromRulesMetadataLogging,
|
withPromRulesMetadataLogging,
|
||||||
withRulerRulesMetadataLogging,
|
withRulerRulesMetadataLogging,
|
||||||
} from '../Analytics';
|
} from '../Analytics';
|
||||||
|
import { alertRuleApi } from '../api/alertRuleApi';
|
||||||
import {
|
import {
|
||||||
deleteAlertManagerConfig,
|
deleteAlertManagerConfig,
|
||||||
fetchAlertGroups,
|
fetchAlertGroups,
|
||||||
@ -51,20 +52,20 @@ import { discoverFeatures } from '../api/buildInfo';
|
|||||||
import { fetchNotifiers } from '../api/grafana';
|
import { fetchNotifiers } from '../api/grafana';
|
||||||
import { FetchPromRulesFilter, fetchRules } from '../api/prometheus';
|
import { FetchPromRulesFilter, fetchRules } from '../api/prometheus';
|
||||||
import {
|
import {
|
||||||
|
FetchRulerRulesFilter,
|
||||||
deleteNamespace,
|
deleteNamespace,
|
||||||
deleteRulerRulesGroup,
|
deleteRulerRulesGroup,
|
||||||
fetchRulerRules,
|
fetchRulerRules,
|
||||||
FetchRulerRulesFilter,
|
|
||||||
setRulerRuleGroup,
|
setRulerRuleGroup,
|
||||||
} from '../api/ruler';
|
} from '../api/ruler';
|
||||||
import { encodeGrafanaNamespace } from '../components/expressions/util';
|
import { encodeGrafanaNamespace } from '../components/expressions/util';
|
||||||
import { RuleFormType, RuleFormValues } from '../types/rule-form';
|
import { RuleFormType, RuleFormValues } from '../types/rule-form';
|
||||||
import { addDefaultsToAlertmanagerConfig, removeMuteTimingFromRoute } from '../utils/alertmanager';
|
import { addDefaultsToAlertmanagerConfig, removeMuteTimingFromRoute } from '../utils/alertmanager';
|
||||||
import {
|
import {
|
||||||
|
GRAFANA_RULES_SOURCE_NAME,
|
||||||
getAllRulesSourceNames,
|
getAllRulesSourceNames,
|
||||||
getRulesDataSource,
|
getRulesDataSource,
|
||||||
getRulesSourceName,
|
getRulesSourceName,
|
||||||
GRAFANA_RULES_SOURCE_NAME,
|
|
||||||
} from '../utils/datasource';
|
} from '../utils/datasource';
|
||||||
import { makeAMLink } from '../utils/misc';
|
import { makeAMLink } from '../utils/misc';
|
||||||
import { AsyncRequestMapSlice, withAppEvents, withSerializedError } from '../utils/redux';
|
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(
|
export function deleteRulesGroupAction(
|
||||||
namespace: CombinedRuleNamespace,
|
namespace: CombinedRuleNamespace,
|
||||||
ruleGroup: CombinedRuleGroup
|
ruleGroup: CombinedRuleGroup
|
||||||
@ -403,6 +396,7 @@ export const saveRuleFormAction = createAsyncThunk(
|
|||||||
// For the dataSourceName specified
|
// For the dataSourceName specified
|
||||||
// in case of system (cortex/loki)
|
// in case of system (cortex/loki)
|
||||||
let identifier: RuleIdentifier;
|
let identifier: RuleIdentifier;
|
||||||
|
|
||||||
if (type === RuleFormType.cloudAlerting || type === RuleFormType.cloudRecording) {
|
if (type === RuleFormType.cloudAlerting || type === RuleFormType.cloudRecording) {
|
||||||
if (!values.dataSourceName) {
|
if (!values.dataSourceName) {
|
||||||
throw new Error('The Data source has not been defined.');
|
throw new Error('The Data source has not been defined.');
|
||||||
@ -412,14 +406,14 @@ export const saveRuleFormAction = createAsyncThunk(
|
|||||||
const rulerClient = getRulerClient(rulerConfig);
|
const rulerClient = getRulerClient(rulerConfig);
|
||||||
identifier = await rulerClient.saveLotexRule(values, evaluateEvery, existing);
|
identifier = await rulerClient.saveLotexRule(values, evaluateEvery, existing);
|
||||||
await thunkAPI.dispatch(fetchRulerRulesAction({ rulesSourceName: values.dataSourceName }));
|
await thunkAPI.dispatch(fetchRulerRulesAction({ rulesSourceName: values.dataSourceName }));
|
||||||
|
|
||||||
// in case of grafana managed
|
// in case of grafana managed
|
||||||
} else if (type === RuleFormType.grafana) {
|
} else if (type === RuleFormType.grafana) {
|
||||||
const rulerConfig = getDataSourceRulerConfig(thunkAPI.getState, GRAFANA_RULES_SOURCE_NAME);
|
const rulerConfig = getDataSourceRulerConfig(thunkAPI.getState, GRAFANA_RULES_SOURCE_NAME);
|
||||||
const rulerClient = getRulerClient(rulerConfig);
|
const rulerClient = getRulerClient(rulerConfig);
|
||||||
identifier = await rulerClient.saveGrafanaRule(values, evaluateEvery, existing);
|
identifier = await rulerClient.saveGrafanaRule(values, evaluateEvery, existing);
|
||||||
reportSwitchingRoutingType(values, 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 {
|
} else {
|
||||||
throw new Error('Unexpected rule form type');
|
throw new Error('Unexpected rule form type');
|
||||||
}
|
}
|
||||||
@ -439,9 +433,6 @@ export const saveRuleFormAction = createAsyncThunk(
|
|||||||
const newLocation = `/alerting/${encodeURIComponent(stringifiedIdentifier)}/edit`;
|
const newLocation = `/alerting/${encodeURIComponent(stringifiedIdentifier)}/edit`;
|
||||||
if (locationService.getLocation().pathname !== newLocation) {
|
if (locationService.getLocation().pathname !== newLocation) {
|
||||||
locationService.replace(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 {
|
import {
|
||||||
deleteAlertManagerConfigAction,
|
deleteAlertManagerConfigAction,
|
||||||
fetchAlertGroupsAction,
|
fetchAlertGroupsAction,
|
||||||
fetchEditableRuleAction,
|
|
||||||
fetchFolderAction,
|
fetchFolderAction,
|
||||||
fetchGrafanaAnnotationsAction,
|
fetchGrafanaAnnotationsAction,
|
||||||
fetchGrafanaNotifiersAction,
|
fetchGrafanaNotifiersAction,
|
||||||
@ -29,7 +28,6 @@ export const reducer = combineReducers({
|
|||||||
.reducer,
|
.reducer,
|
||||||
ruleForm: combineReducers({
|
ruleForm: combineReducers({
|
||||||
saveRule: createAsyncSlice('saveRule', saveRuleFormAction).reducer,
|
saveRule: createAsyncSlice('saveRule', saveRuleFormAction).reducer,
|
||||||
existingRule: createAsyncSlice('existingRule', fetchEditableRuleAction).reducer,
|
|
||||||
}),
|
}),
|
||||||
grafanaNotifiers: createAsyncSlice('grafanaNotifiers', fetchGrafanaNotifiersAction).reducer,
|
grafanaNotifiers: createAsyncSlice('grafanaNotifiers', fetchGrafanaNotifiersAction).reducer,
|
||||||
saveAMConfig: createAsyncSlice('saveAMConfig', updateAlertManagerConfigAction).reducer,
|
saveAMConfig: createAsyncSlice('saveAMConfig', updateAlertManagerConfigAction).reducer,
|
||||||
|
@ -89,6 +89,7 @@ const grafanaAlert = {
|
|||||||
condition: 'B',
|
condition: 'B',
|
||||||
exec_err_state: GrafanaAlertStateDecision.Alerting,
|
exec_err_state: GrafanaAlertStateDecision.Alerting,
|
||||||
namespace_uid: 'namespaceuid123',
|
namespace_uid: 'namespaceuid123',
|
||||||
|
rule_group: 'my-group',
|
||||||
no_data_state: GrafanaAlertStateDecision.NoData,
|
no_data_state: GrafanaAlertStateDecision.NoData,
|
||||||
title: 'Test alert',
|
title: 'Test alert',
|
||||||
uid: 'asdf23',
|
uid: 'asdf23',
|
||||||
|
@ -97,6 +97,7 @@ describe('getContactPointsFromDTO', () => {
|
|||||||
uid: '123',
|
uid: '123',
|
||||||
title: 'myalert',
|
title: 'myalert',
|
||||||
namespace_uid: '123',
|
namespace_uid: '123',
|
||||||
|
rule_group: 'my-group',
|
||||||
condition: 'A',
|
condition: 'A',
|
||||||
no_data_state: GrafanaAlertStateDecision.Alerting,
|
no_data_state: GrafanaAlertStateDecision.Alerting,
|
||||||
exec_err_state: GrafanaAlertStateDecision.Alerting,
|
exec_err_state: GrafanaAlertStateDecision.Alerting,
|
||||||
@ -120,6 +121,7 @@ describe('getContactPointsFromDTO', () => {
|
|||||||
uid: '123',
|
uid: '123',
|
||||||
title: 'myalert',
|
title: 'myalert',
|
||||||
namespace_uid: '123',
|
namespace_uid: '123',
|
||||||
|
rule_group: 'my-group',
|
||||||
condition: 'A',
|
condition: 'A',
|
||||||
no_data_state: GrafanaAlertStateDecision.Alerting,
|
no_data_state: GrafanaAlertStateDecision.Alerting,
|
||||||
exec_err_state: GrafanaAlertStateDecision.Alerting,
|
exec_err_state: GrafanaAlertStateDecision.Alerting,
|
||||||
|
@ -46,6 +46,7 @@ describe('hashRulerRule', () => {
|
|||||||
const grafanaAlertDefinition: GrafanaRuleDefinition = {
|
const grafanaAlertDefinition: GrafanaRuleDefinition = {
|
||||||
uid: RULE_UID,
|
uid: RULE_UID,
|
||||||
namespace_uid: 'namespace',
|
namespace_uid: 'namespace',
|
||||||
|
rule_group: 'my-group',
|
||||||
title: 'my rule',
|
title: 'my rule',
|
||||||
condition: '',
|
condition: '',
|
||||||
data: [],
|
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 {
|
import {
|
||||||
PostableRuleGrafanaRuleDTO,
|
PostableRuleGrafanaRuleDTO,
|
||||||
PostableRulerRuleGroupDTO,
|
PostableRulerRuleGroupDTO,
|
||||||
@ -21,9 +26,16 @@ import {
|
|||||||
|
|
||||||
export interface RulerClient {
|
export interface RulerClient {
|
||||||
findEditableRule(ruleIdentifier: RuleIdentifier): Promise<RuleWithLocation | null>;
|
findEditableRule(ruleIdentifier: RuleIdentifier): Promise<RuleWithLocation | null>;
|
||||||
|
|
||||||
deleteRule(ruleWithLocation: RuleWithLocation): Promise<void>;
|
deleteRule(ruleWithLocation: RuleWithLocation): Promise<void>;
|
||||||
|
|
||||||
saveLotexRule(values: RuleFormValues, evaluateEvery: string, existing?: RuleWithLocation): Promise<RuleIdentifier>;
|
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 {
|
export function getRulerClient(rulerConfig: RulerDataSourceConfig): RulerClient {
|
||||||
@ -153,7 +165,7 @@ export function getRulerClient(rulerConfig: RulerDataSourceConfig): RulerClient
|
|||||||
values: RuleFormValues,
|
values: RuleFormValues,
|
||||||
evaluateEvery: string,
|
evaluateEvery: string,
|
||||||
existingRule?: RuleWithLocation
|
existingRule?: RuleWithLocation
|
||||||
): Promise<RuleIdentifier> => {
|
): Promise<GrafanaRuleIdentifier> => {
|
||||||
const { folder, group } = values;
|
const { folder, group } = values;
|
||||||
if (!folder) {
|
if (!folder) {
|
||||||
throw new Error('Folder must be specified');
|
throw new Error('Folder must be specified');
|
||||||
@ -190,7 +202,7 @@ export function getRulerClient(rulerConfig: RulerDataSourceConfig): RulerClient
|
|||||||
namespaceUID: string,
|
namespaceUID: string,
|
||||||
group: { name: string; interval: string },
|
group: { name: string; interval: string },
|
||||||
newRule: PostableRuleGrafanaRuleDTO
|
newRule: PostableRuleGrafanaRuleDTO
|
||||||
): Promise<RuleIdentifier> => {
|
): Promise<GrafanaRuleIdentifier> => {
|
||||||
const existingGroup = await fetchRulerRulesGroup(rulerConfig, namespaceUID, group.name);
|
const existingGroup = await fetchRulerRulesGroup(rulerConfig, namespaceUID, group.name);
|
||||||
if (!existingGroup) {
|
if (!existingGroup) {
|
||||||
throw new Error(`No group found with name "${group.name}"`);
|
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 },
|
group: { name: string; interval: string },
|
||||||
existingRule: RuleWithLocation,
|
existingRule: RuleWithLocation,
|
||||||
newRule: PostableRuleGrafanaRuleDTO
|
newRule: PostableRuleGrafanaRuleDTO
|
||||||
): Promise<RuleIdentifier> => {
|
): Promise<GrafanaRuleIdentifier> => {
|
||||||
// make sure our updated alert has the same UID as before
|
// make sure our updated alert has the same UID as before
|
||||||
// that way the rule is automatically moved to the new namespace / group name
|
// that way the rule is automatically moved to the new namespace / group name
|
||||||
copyGrafanaUID(existingRule, newRule);
|
copyGrafanaUID(existingRule, newRule);
|
||||||
@ -228,7 +240,7 @@ export function getRulerClient(rulerConfig: RulerDataSourceConfig): RulerClient
|
|||||||
existingRule: RuleWithLocation,
|
existingRule: RuleWithLocation,
|
||||||
newRule: PostableRuleGrafanaRuleDTO,
|
newRule: PostableRuleGrafanaRuleDTO,
|
||||||
interval: string
|
interval: string
|
||||||
): Promise<RuleIdentifier> => {
|
): Promise<GrafanaRuleIdentifier> => {
|
||||||
// make sure our updated alert has the same UID as before
|
// make sure our updated alert has the same UID as before
|
||||||
copyGrafanaUID(existingRule, newRule);
|
copyGrafanaUID(existingRule, newRule);
|
||||||
|
|
||||||
|
@ -44,6 +44,7 @@ export function getRulerRulesResponse(folderName: string, folderUid: string, see
|
|||||||
],
|
],
|
||||||
uid: random.guid(),
|
uid: random.guid(),
|
||||||
namespace_uid: folderUid,
|
namespace_uid: folderUid,
|
||||||
|
rule_group: 'my-group',
|
||||||
no_data_state: GrafanaAlertStateDecision.NoData,
|
no_data_state: GrafanaAlertStateDecision.NoData,
|
||||||
exec_err_state: GrafanaAlertStateDecision.Error,
|
exec_err_state: GrafanaAlertStateDecision.Error,
|
||||||
is_paused: false,
|
is_paused: false,
|
||||||
|
@ -221,6 +221,7 @@ export interface GrafanaRuleDefinition extends PostableGrafanaRuleDefinition {
|
|||||||
id?: string;
|
id?: string;
|
||||||
uid: string;
|
uid: string;
|
||||||
namespace_uid: string;
|
namespace_uid: string;
|
||||||
|
rule_group: string;
|
||||||
provenance?: string;
|
provenance?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -139,9 +139,9 @@ export interface CombinedRuleNamespace {
|
|||||||
export interface RuleWithLocation<T = RulerRuleDTO> {
|
export interface RuleWithLocation<T = RulerRuleDTO> {
|
||||||
ruleSourceName: string;
|
ruleSourceName: string;
|
||||||
namespace: string;
|
namespace: string;
|
||||||
|
namespace_uid?: string; // Grafana folder UID
|
||||||
group: RulerRuleGroupDTO;
|
group: RulerRuleGroupDTO;
|
||||||
rule: T;
|
rule: T;
|
||||||
namespace_uid?: string;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface CombinedRuleWithLocation extends CombinedRule {
|
export interface CombinedRuleWithLocation extends CombinedRule {
|
||||||
|
@ -10,6 +10,7 @@ import RuleEditor from 'app/features/alerting/unified/RuleEditor';
|
|||||||
import { TestProvider } from './TestProvider';
|
import { TestProvider } from './TestProvider';
|
||||||
|
|
||||||
export const ui = {
|
export const ui = {
|
||||||
|
loadingIndicator: byText('Loading rule...'),
|
||||||
inputs: {
|
inputs: {
|
||||||
name: byRole('textbox', { name: 'name' }),
|
name: byRole('textbox', { name: 'name' }),
|
||||||
alertType: byTestId('alert-type-picker'),
|
alertType: byTestId('alert-type-picker'),
|
||||||
|
Loading…
Reference in New Issue
Block a user