mirror of
https://github.com/grafana/grafana.git
synced 2025-02-25 18:55:37 -06:00
Alerting: Re-organise rule group hooks (#90368)
This commit is contained in:
parent
8400b54a53
commit
e64ef2245c
@ -7,8 +7,8 @@ import {
|
||||
} from 'app/features/alerting/unified/utils/rules';
|
||||
import { CombinedRule } from 'app/types/unified-alerting';
|
||||
|
||||
import { usePauseRuleInGroup } from '../hooks/ruleGroup/usePauseAlertRule';
|
||||
import { isLoading } from '../hooks/useAsync';
|
||||
import { usePauseRuleInGroup } from '../hooks/useProduceNewRuleGroup';
|
||||
import { stringifyErrorLike } from '../utils/misc';
|
||||
|
||||
interface Props {
|
||||
|
@ -27,7 +27,7 @@ import {
|
||||
trackAlertRuleFormCancelled,
|
||||
trackAlertRuleFormSaved,
|
||||
} from '../../../Analytics';
|
||||
import { useDeleteRuleFromGroup } from '../../../hooks/useProduceNewRuleGroup';
|
||||
import { useDeleteRuleFromGroup } from '../../../hooks/ruleGroup/useDeleteRuleFromGroup';
|
||||
import { useUnifiedAlertingSelector } from '../../../hooks/useUnifiedAlertingSelector';
|
||||
import { saveRuleFormAction } from '../../../state/actions';
|
||||
import { RuleFormType, RuleFormValues } from '../../../types/rule-form';
|
||||
|
@ -5,7 +5,7 @@ import { ConfirmModal } from '@grafana/ui';
|
||||
import { dispatch } from 'app/store/store';
|
||||
import { CombinedRule } from 'app/types/unified-alerting';
|
||||
|
||||
import { useDeleteRuleFromGroup } from '../../hooks/useProduceNewRuleGroup';
|
||||
import { useDeleteRuleFromGroup } from '../../hooks/ruleGroup/useDeleteRuleFromGroup';
|
||||
import { fetchPromAndRulerRulesAction } from '../../state/actions';
|
||||
import { getRuleGroupLocationFromCombinedRule } from '../../utils/rules';
|
||||
|
||||
|
@ -10,12 +10,12 @@ import { dispatch } from 'app/store/store';
|
||||
import { CombinedRuleGroup, CombinedRuleNamespace, RuleGroupIdentifier } from 'app/types/unified-alerting';
|
||||
import { RulerRuleDTO } from 'app/types/unified-alerting-dto';
|
||||
|
||||
import { anyOfRequestState } from '../../hooks/useAsync';
|
||||
import {
|
||||
useMoveRuleGroup,
|
||||
useRenameRuleGroup,
|
||||
useUpdateRuleGroupConfiguration,
|
||||
} from '../../hooks/useProduceNewRuleGroup';
|
||||
useRenameRuleGroup,
|
||||
useMoveRuleGroup,
|
||||
} from '../../hooks/ruleGroup/useUpdateRuleGroup';
|
||||
import { anyOfRequestState } from '../../hooks/useAsync';
|
||||
import { fetchRulerRulesAction, rulesInSameGroupHaveInvalidFor } from '../../state/actions';
|
||||
import { checkEvaluationIntervalGlobalLimit } from '../../utils/config';
|
||||
import { getRulesSourceName, GRAFANA_RULES_SOURCE_NAME } from '../../utils/datasource';
|
||||
|
@ -0,0 +1,156 @@
|
||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`delete rule should be able to delete a Data source managed rule 1`] = `
|
||||
[
|
||||
{
|
||||
"body": "",
|
||||
"headers": [
|
||||
[
|
||||
"accept",
|
||||
"application/json, text/plain, */*",
|
||||
],
|
||||
],
|
||||
"method": "GET",
|
||||
"url": "http://localhost/api/ruler/grafana/api/v1/rules/mockCombinedNamespace/mockCombinedRuleGroup?subtype=cortex",
|
||||
},
|
||||
{
|
||||
"body": {
|
||||
"name": "group-1",
|
||||
"rules": [
|
||||
{
|
||||
"alert": "r1",
|
||||
"annotations": {
|
||||
"summary": "test alert",
|
||||
},
|
||||
"expr": "up = 1",
|
||||
"labels": {
|
||||
"foo": "bar",
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
"headers": [
|
||||
[
|
||||
"content-type",
|
||||
"application/json",
|
||||
],
|
||||
[
|
||||
"accept",
|
||||
"application/json, text/plain, */*",
|
||||
],
|
||||
],
|
||||
"method": "POST",
|
||||
"url": "http://localhost/api/ruler/grafana/api/v1/rules/mockCombinedNamespace?subtype=cortex",
|
||||
},
|
||||
{
|
||||
"body": "",
|
||||
"headers": [
|
||||
[
|
||||
"accept",
|
||||
"application/json, text/plain, */*",
|
||||
],
|
||||
],
|
||||
"method": "GET",
|
||||
"url": "http://localhost/api/ruler/grafana/api/v1/rules/mockCombinedNamespace/mockCombinedRuleGroup?subtype=cortex",
|
||||
},
|
||||
]
|
||||
`;
|
||||
|
||||
exports[`delete rule should be able to delete a Grafana managed rule 1`] = `
|
||||
[
|
||||
{
|
||||
"body": "",
|
||||
"headers": [
|
||||
[
|
||||
"accept",
|
||||
"application/json, text/plain, */*",
|
||||
],
|
||||
],
|
||||
"method": "GET",
|
||||
"url": "http://localhost/api/ruler/grafana/api/v1/rules/NAMESPACE_UID/mockCombinedRuleGroup?subtype=cortex",
|
||||
},
|
||||
{
|
||||
"body": {
|
||||
"name": "group-1",
|
||||
"rules": [
|
||||
{
|
||||
"annotations": {},
|
||||
"for": "",
|
||||
"grafana_alert": {
|
||||
"condition": "",
|
||||
"data": [],
|
||||
"exec_err_state": "Error",
|
||||
"namespace_uid": "NAMESPACE_UID",
|
||||
"no_data_state": "NoData",
|
||||
"rule_group": "my-group",
|
||||
"title": "my rule",
|
||||
"uid": "r1",
|
||||
},
|
||||
"labels": {},
|
||||
},
|
||||
],
|
||||
},
|
||||
"headers": [
|
||||
[
|
||||
"content-type",
|
||||
"application/json",
|
||||
],
|
||||
[
|
||||
"accept",
|
||||
"application/json, text/plain, */*",
|
||||
],
|
||||
],
|
||||
"method": "POST",
|
||||
"url": "http://localhost/api/ruler/grafana/api/v1/rules/NAMESPACE_UID?subtype=cortex",
|
||||
},
|
||||
{
|
||||
"body": "",
|
||||
"headers": [
|
||||
[
|
||||
"accept",
|
||||
"application/json, text/plain, */*",
|
||||
],
|
||||
],
|
||||
"method": "GET",
|
||||
"url": "http://localhost/api/ruler/grafana/api/v1/rules/NAMESPACE_UID/mockCombinedRuleGroup?subtype=cortex",
|
||||
},
|
||||
]
|
||||
`;
|
||||
|
||||
exports[`delete rule should delete the entire group if no more rules are left 1`] = `
|
||||
[
|
||||
{
|
||||
"body": "",
|
||||
"headers": [
|
||||
[
|
||||
"accept",
|
||||
"application/json, text/plain, */*",
|
||||
],
|
||||
],
|
||||
"method": "GET",
|
||||
"url": "http://localhost/api/ruler/grafana/api/v1/rules/uuid020c61ef/mockCombinedRuleGroup?subtype=cortex",
|
||||
},
|
||||
{
|
||||
"body": "",
|
||||
"headers": [
|
||||
[
|
||||
"accept",
|
||||
"application/json, text/plain, */*",
|
||||
],
|
||||
],
|
||||
"method": "DELETE",
|
||||
"url": "http://localhost/api/ruler/grafana/api/v1/rules/uuid020c61ef/mockCombinedRuleGroup?subtype=cortex",
|
||||
},
|
||||
{
|
||||
"body": "",
|
||||
"headers": [
|
||||
[
|
||||
"accept",
|
||||
"application/json, text/plain, */*",
|
||||
],
|
||||
],
|
||||
"method": "GET",
|
||||
"url": "http://localhost/api/ruler/grafana/api/v1/rules/uuid020c61ef/mockCombinedRuleGroup?subtype=cortex",
|
||||
},
|
||||
]
|
||||
`;
|
@ -0,0 +1,88 @@
|
||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`pause rule should be able to pause a rule 1`] = `
|
||||
[
|
||||
{
|
||||
"body": "",
|
||||
"headers": [
|
||||
[
|
||||
"accept",
|
||||
"application/json, text/plain, */*",
|
||||
],
|
||||
],
|
||||
"method": "GET",
|
||||
"url": "http://localhost/api/ruler/grafana/api/v1/rules/uuid020c61ef/grafana-group-1?subtype=cortex",
|
||||
},
|
||||
{
|
||||
"body": {
|
||||
"interval": "1m",
|
||||
"name": "grafana-group-1",
|
||||
"rules": [
|
||||
{
|
||||
"annotations": {
|
||||
"summary": "Test alert",
|
||||
},
|
||||
"for": "5m",
|
||||
"grafana_alert": {
|
||||
"condition": "A",
|
||||
"data": [
|
||||
{
|
||||
"datasourceUid": "datasource-uid",
|
||||
"model": {
|
||||
"datasource": {
|
||||
"type": "prometheus",
|
||||
"uid": "datasource-uid",
|
||||
},
|
||||
"expression": "vector(1)",
|
||||
"queryType": "alerting",
|
||||
"refId": "A",
|
||||
},
|
||||
"queryType": "alerting",
|
||||
"refId": "A",
|
||||
"relativeTimeRange": {
|
||||
"from": 1000,
|
||||
"to": 2000,
|
||||
},
|
||||
},
|
||||
],
|
||||
"exec_err_state": "Error",
|
||||
"is_paused": true,
|
||||
"namespace_uid": "uuid020c61ef",
|
||||
"no_data_state": "NoData",
|
||||
"rule_group": "grafana-group-1",
|
||||
"title": "Grafana-rule",
|
||||
"uid": "4d7125fee983",
|
||||
},
|
||||
"labels": {
|
||||
"region": "nasa",
|
||||
"severity": "critical",
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
"headers": [
|
||||
[
|
||||
"content-type",
|
||||
"application/json",
|
||||
],
|
||||
[
|
||||
"accept",
|
||||
"application/json, text/plain, */*",
|
||||
],
|
||||
],
|
||||
"method": "POST",
|
||||
"url": "http://localhost/api/ruler/grafana/api/v1/rules/uuid020c61ef?subtype=cortex",
|
||||
},
|
||||
{
|
||||
"body": "",
|
||||
"headers": [
|
||||
[
|
||||
"accept",
|
||||
"application/json, text/plain, */*",
|
||||
],
|
||||
],
|
||||
"method": "GET",
|
||||
"url": "http://localhost/api/ruler/grafana/api/v1/rules/uuid020c61ef/grafana-group-1?subtype=cortex",
|
||||
},
|
||||
]
|
||||
`;
|
@ -1,247 +1,5 @@
|
||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`delete rule should be able to delete a Data source managed rule 1`] = `
|
||||
[
|
||||
{
|
||||
"body": "",
|
||||
"headers": [
|
||||
[
|
||||
"accept",
|
||||
"application/json, text/plain, */*",
|
||||
],
|
||||
],
|
||||
"method": "GET",
|
||||
"url": "http://localhost/api/ruler/grafana/api/v1/rules/mockCombinedNamespace/mockCombinedRuleGroup?subtype=cortex",
|
||||
},
|
||||
{
|
||||
"body": {
|
||||
"name": "group-1",
|
||||
"rules": [
|
||||
{
|
||||
"alert": "r1",
|
||||
"annotations": {
|
||||
"summary": "test alert",
|
||||
},
|
||||
"expr": "up = 1",
|
||||
"labels": {
|
||||
"foo": "bar",
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
"headers": [
|
||||
[
|
||||
"content-type",
|
||||
"application/json",
|
||||
],
|
||||
[
|
||||
"accept",
|
||||
"application/json, text/plain, */*",
|
||||
],
|
||||
],
|
||||
"method": "POST",
|
||||
"url": "http://localhost/api/ruler/grafana/api/v1/rules/mockCombinedNamespace?subtype=cortex",
|
||||
},
|
||||
{
|
||||
"body": "",
|
||||
"headers": [
|
||||
[
|
||||
"accept",
|
||||
"application/json, text/plain, */*",
|
||||
],
|
||||
],
|
||||
"method": "GET",
|
||||
"url": "http://localhost/api/ruler/grafana/api/v1/rules/mockCombinedNamespace/mockCombinedRuleGroup?subtype=cortex",
|
||||
},
|
||||
]
|
||||
`;
|
||||
|
||||
exports[`delete rule should be able to delete a Grafana managed rule 1`] = `
|
||||
[
|
||||
{
|
||||
"body": "",
|
||||
"headers": [
|
||||
[
|
||||
"accept",
|
||||
"application/json, text/plain, */*",
|
||||
],
|
||||
],
|
||||
"method": "GET",
|
||||
"url": "http://localhost/api/ruler/grafana/api/v1/rules/NAMESPACE_UID/mockCombinedRuleGroup?subtype=cortex",
|
||||
},
|
||||
{
|
||||
"body": {
|
||||
"name": "group-1",
|
||||
"rules": [
|
||||
{
|
||||
"annotations": {},
|
||||
"for": "",
|
||||
"grafana_alert": {
|
||||
"condition": "",
|
||||
"data": [],
|
||||
"exec_err_state": "Error",
|
||||
"namespace_uid": "NAMESPACE_UID",
|
||||
"no_data_state": "NoData",
|
||||
"rule_group": "my-group",
|
||||
"title": "my rule",
|
||||
"uid": "r1",
|
||||
},
|
||||
"labels": {},
|
||||
},
|
||||
],
|
||||
},
|
||||
"headers": [
|
||||
[
|
||||
"content-type",
|
||||
"application/json",
|
||||
],
|
||||
[
|
||||
"accept",
|
||||
"application/json, text/plain, */*",
|
||||
],
|
||||
],
|
||||
"method": "POST",
|
||||
"url": "http://localhost/api/ruler/grafana/api/v1/rules/NAMESPACE_UID?subtype=cortex",
|
||||
},
|
||||
{
|
||||
"body": "",
|
||||
"headers": [
|
||||
[
|
||||
"accept",
|
||||
"application/json, text/plain, */*",
|
||||
],
|
||||
],
|
||||
"method": "GET",
|
||||
"url": "http://localhost/api/ruler/grafana/api/v1/rules/NAMESPACE_UID/mockCombinedRuleGroup?subtype=cortex",
|
||||
},
|
||||
]
|
||||
`;
|
||||
|
||||
exports[`delete rule should delete the entire group if no more rules are left 1`] = `
|
||||
[
|
||||
{
|
||||
"body": "",
|
||||
"headers": [
|
||||
[
|
||||
"accept",
|
||||
"application/json, text/plain, */*",
|
||||
],
|
||||
],
|
||||
"method": "GET",
|
||||
"url": "http://localhost/api/ruler/grafana/api/v1/rules/uuid020c61ef/mockCombinedRuleGroup?subtype=cortex",
|
||||
},
|
||||
{
|
||||
"body": "",
|
||||
"headers": [
|
||||
[
|
||||
"accept",
|
||||
"application/json, text/plain, */*",
|
||||
],
|
||||
],
|
||||
"method": "DELETE",
|
||||
"url": "http://localhost/api/ruler/grafana/api/v1/rules/uuid020c61ef/mockCombinedRuleGroup?subtype=cortex",
|
||||
},
|
||||
{
|
||||
"body": "",
|
||||
"headers": [
|
||||
[
|
||||
"accept",
|
||||
"application/json, text/plain, */*",
|
||||
],
|
||||
],
|
||||
"method": "GET",
|
||||
"url": "http://localhost/api/ruler/grafana/api/v1/rules/uuid020c61ef/mockCombinedRuleGroup?subtype=cortex",
|
||||
},
|
||||
]
|
||||
`;
|
||||
|
||||
exports[`pause rule should be able to pause a rule 1`] = `
|
||||
[
|
||||
{
|
||||
"body": "",
|
||||
"headers": [
|
||||
[
|
||||
"accept",
|
||||
"application/json, text/plain, */*",
|
||||
],
|
||||
],
|
||||
"method": "GET",
|
||||
"url": "http://localhost/api/ruler/grafana/api/v1/rules/uuid020c61ef/grafana-group-1?subtype=cortex",
|
||||
},
|
||||
{
|
||||
"body": {
|
||||
"interval": "1m",
|
||||
"name": "grafana-group-1",
|
||||
"rules": [
|
||||
{
|
||||
"annotations": {
|
||||
"summary": "Test alert",
|
||||
},
|
||||
"for": "5m",
|
||||
"grafana_alert": {
|
||||
"condition": "A",
|
||||
"data": [
|
||||
{
|
||||
"datasourceUid": "datasource-uid",
|
||||
"model": {
|
||||
"datasource": {
|
||||
"type": "prometheus",
|
||||
"uid": "datasource-uid",
|
||||
},
|
||||
"expression": "vector(1)",
|
||||
"queryType": "alerting",
|
||||
"refId": "A",
|
||||
},
|
||||
"queryType": "alerting",
|
||||
"refId": "A",
|
||||
"relativeTimeRange": {
|
||||
"from": 1000,
|
||||
"to": 2000,
|
||||
},
|
||||
},
|
||||
],
|
||||
"exec_err_state": "Error",
|
||||
"is_paused": true,
|
||||
"namespace_uid": "uuid020c61ef",
|
||||
"no_data_state": "NoData",
|
||||
"rule_group": "grafana-group-1",
|
||||
"title": "Grafana-rule",
|
||||
"uid": "4d7125fee983",
|
||||
},
|
||||
"labels": {
|
||||
"region": "nasa",
|
||||
"severity": "critical",
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
"headers": [
|
||||
[
|
||||
"content-type",
|
||||
"application/json",
|
||||
],
|
||||
[
|
||||
"accept",
|
||||
"application/json, text/plain, */*",
|
||||
],
|
||||
],
|
||||
"method": "POST",
|
||||
"url": "http://localhost/api/ruler/grafana/api/v1/rules/uuid020c61ef?subtype=cortex",
|
||||
},
|
||||
{
|
||||
"body": "",
|
||||
"headers": [
|
||||
[
|
||||
"accept",
|
||||
"application/json, text/plain, */*",
|
||||
],
|
||||
],
|
||||
"method": "GET",
|
||||
"url": "http://localhost/api/ruler/grafana/api/v1/rules/uuid020c61ef/grafana-group-1?subtype=cortex",
|
||||
},
|
||||
]
|
||||
`;
|
||||
|
||||
exports[`useUpdateRuleGroupConfiguration should be able to move a Data Source managed rule group 1`] = `
|
||||
[
|
||||
{
|
@ -0,0 +1,152 @@
|
||||
import userEvent from '@testing-library/user-event';
|
||||
import { HttpResponse } from 'msw';
|
||||
import { render } from 'test/test-utils';
|
||||
import { byRole, byText } from 'testing-library-selector';
|
||||
|
||||
import { setBackendSrv } from '@grafana/runtime';
|
||||
import { backendSrv } from 'app/core/services/backend_srv';
|
||||
import { AccessControlAction } from 'app/types';
|
||||
import { CombinedRule } from 'app/types/unified-alerting';
|
||||
|
||||
import server, { setupMswServer } from '../../mockApi';
|
||||
import {
|
||||
mockCombinedRule,
|
||||
mockGrafanaRulerRule,
|
||||
mockRulerRuleGroup,
|
||||
mockRulerAlertingRule,
|
||||
mockRulerRecordingRule,
|
||||
grantUserPermissions,
|
||||
} from '../../mocks';
|
||||
import { grafanaRulerRule } from '../../mocks/grafanaRulerApi';
|
||||
import { setUpdateRulerRuleNamespaceHandler, setRulerRuleGroupHandler } from '../../mocks/server/configure';
|
||||
import { captureRequests, serializeRequests } from '../../mocks/server/events';
|
||||
import { rulerRuleGroupHandler, updateRulerRuleNamespaceHandler } from '../../mocks/server/handlers/mimirRuler';
|
||||
import { getRuleGroupLocationFromCombinedRule } from '../../utils/rules';
|
||||
import { SerializeState } from '../useAsync';
|
||||
|
||||
import { useDeleteRuleFromGroup } from './useDeleteRuleFromGroup';
|
||||
|
||||
setupMswServer();
|
||||
|
||||
beforeAll(() => {
|
||||
setBackendSrv(backendSrv);
|
||||
grantUserPermissions([AccessControlAction.AlertingRuleExternalRead, AccessControlAction.AlertingRuleRead]);
|
||||
});
|
||||
|
||||
describe('delete rule', () => {
|
||||
it('should be able to delete a Grafana managed rule', async () => {
|
||||
const rules = [
|
||||
mockCombinedRule({
|
||||
name: 'r1',
|
||||
rulerRule: mockGrafanaRulerRule({ uid: 'r1' }),
|
||||
}),
|
||||
mockCombinedRule({
|
||||
name: 'r2',
|
||||
rulerRule: mockGrafanaRulerRule({ uid: 'r2' }),
|
||||
}),
|
||||
];
|
||||
const group = mockRulerRuleGroup({
|
||||
name: 'group-1',
|
||||
rules: [rules[0].rulerRule!, rules[1].rulerRule!],
|
||||
});
|
||||
|
||||
const getGroup = rulerRuleGroupHandler({
|
||||
delay: 0,
|
||||
response: HttpResponse.json(group),
|
||||
});
|
||||
|
||||
const updateNamespace = updateRulerRuleNamespaceHandler({
|
||||
response: new HttpResponse(undefined, { status: 200 }),
|
||||
});
|
||||
|
||||
server.use(getGroup, updateNamespace);
|
||||
|
||||
const capture = captureRequests();
|
||||
|
||||
render(<DeleteTestComponent rule={rules[1]} />);
|
||||
|
||||
await userEvent.click(byRole('button').get());
|
||||
|
||||
expect(await byText(/success/i).find()).toBeInTheDocument();
|
||||
|
||||
const requests = await capture;
|
||||
const serializedRequests = await serializeRequests(requests);
|
||||
expect(serializedRequests).toMatchSnapshot();
|
||||
});
|
||||
|
||||
it('should be able to delete a Data source managed rule', async () => {
|
||||
setUpdateRulerRuleNamespaceHandler({
|
||||
response: new HttpResponse(undefined, { status: 200 }),
|
||||
});
|
||||
|
||||
const rules = [
|
||||
mockCombinedRule({
|
||||
name: 'r1',
|
||||
rulerRule: mockRulerAlertingRule({ alert: 'r1', labels: { foo: 'bar' } }),
|
||||
}),
|
||||
mockCombinedRule({
|
||||
name: 'r2',
|
||||
rulerRule: mockRulerRecordingRule({ record: 'r2', labels: { bar: 'baz' } }),
|
||||
}),
|
||||
];
|
||||
|
||||
const group = mockRulerRuleGroup({
|
||||
name: 'group-1',
|
||||
rules: [rules[0].rulerRule!, rules[1].rulerRule!],
|
||||
});
|
||||
|
||||
setRulerRuleGroupHandler({
|
||||
delay: 0,
|
||||
response: HttpResponse.json(group),
|
||||
});
|
||||
|
||||
const capture = captureRequests();
|
||||
|
||||
render(<DeleteTestComponent rule={rules[1]} />);
|
||||
|
||||
await userEvent.click(byRole('button').get());
|
||||
|
||||
expect(await byText(/success/i).find()).toBeInTheDocument();
|
||||
|
||||
const requests = await capture;
|
||||
const serializedRequests = await serializeRequests(requests);
|
||||
expect(serializedRequests).toMatchSnapshot();
|
||||
});
|
||||
|
||||
it('should delete the entire group if no more rules are left', async () => {
|
||||
const capture = captureRequests();
|
||||
|
||||
const combined = mockCombinedRule({
|
||||
rulerRule: grafanaRulerRule,
|
||||
});
|
||||
|
||||
render(<DeleteTestComponent rule={combined} />);
|
||||
await userEvent.click(byRole('button').get());
|
||||
|
||||
expect(await byText(/success/i).find()).toBeInTheDocument();
|
||||
|
||||
const requests = await capture;
|
||||
const serializedRequests = await serializeRequests(requests);
|
||||
expect(serializedRequests).toMatchSnapshot();
|
||||
});
|
||||
});
|
||||
|
||||
type DeleteTestComponentProps = {
|
||||
rule: CombinedRule;
|
||||
};
|
||||
const DeleteTestComponent = ({ rule }: DeleteTestComponentProps) => {
|
||||
const [requestState, deleteRuleFromGroup] = useDeleteRuleFromGroup();
|
||||
|
||||
// always handle your errors!
|
||||
const ruleGroupID = getRuleGroupLocationFromCombinedRule(rule);
|
||||
const onClick = () => {
|
||||
deleteRuleFromGroup.execute(ruleGroupID, rule.rulerRule!);
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<button onClick={() => onClick()} />
|
||||
<SerializeState state={requestState} />
|
||||
</>
|
||||
);
|
||||
};
|
@ -0,0 +1,49 @@
|
||||
import { t } from 'i18next';
|
||||
|
||||
import { RuleGroupIdentifier } from 'app/types/unified-alerting';
|
||||
import { RulerRuleDTO } from 'app/types/unified-alerting-dto';
|
||||
|
||||
import { alertRuleApi } from '../../api/alertRuleApi';
|
||||
import { deleteRuleAction } from '../../reducers/ruler/ruleGroups';
|
||||
import { useAsync } from '../useAsync';
|
||||
|
||||
import { useProduceNewRuleGroup } from './useProduceNewRuleGroup';
|
||||
|
||||
/**
|
||||
* Delete a single rule from a (ruler) group. This hook will ensure that mutations on the rule group are safe and will always
|
||||
* use the latest definition of the ruler group identifier.
|
||||
*
|
||||
* If no more rules are left in the group it will remove the entire group instead of updating.
|
||||
*/
|
||||
export function useDeleteRuleFromGroup() {
|
||||
const [produceNewRuleGroup] = useProduceNewRuleGroup();
|
||||
const [upsertRuleGroup] = alertRuleApi.endpoints.upsertRuleGroupForNamespace.useMutation();
|
||||
const [deleteRuleGroup] = alertRuleApi.endpoints.deleteRuleGroupFromNamespace.useMutation();
|
||||
|
||||
return useAsync(async (ruleGroup: RuleGroupIdentifier, rule: RulerRuleDTO) => {
|
||||
const { groupName, namespaceName } = ruleGroup;
|
||||
|
||||
const action = deleteRuleAction({ rule });
|
||||
const { newRuleGroupDefinition, rulerConfig } = await produceNewRuleGroup(ruleGroup, action);
|
||||
|
||||
const successMessage = t('alerting.rules.delete-rule.success', 'Rule successfully deleted');
|
||||
|
||||
// if we have no more rules left after reducing, remove the entire group
|
||||
if (newRuleGroupDefinition.rules.length === 0) {
|
||||
return deleteRuleGroup({
|
||||
rulerConfig,
|
||||
namespace: namespaceName,
|
||||
group: groupName,
|
||||
requestOptions: { successMessage },
|
||||
}).unwrap();
|
||||
}
|
||||
|
||||
// otherwise just update the group
|
||||
return upsertRuleGroup({
|
||||
rulerConfig,
|
||||
namespace: namespaceName,
|
||||
payload: newRuleGroupDefinition,
|
||||
requestOptions: { successMessage },
|
||||
}).unwrap();
|
||||
});
|
||||
}
|
@ -0,0 +1,102 @@
|
||||
import userEvent from '@testing-library/user-event';
|
||||
import { HttpResponse } from 'msw';
|
||||
import { render } from 'test/test-utils';
|
||||
import { byText, byRole } from 'testing-library-selector';
|
||||
|
||||
import { setBackendSrv } from '@grafana/runtime';
|
||||
import { backendSrv } from 'app/core/services/backend_srv';
|
||||
import { AccessControlAction } from 'app/types';
|
||||
import { RulerGrafanaRuleDTO } from 'app/types/unified-alerting-dto';
|
||||
|
||||
import { setupMswServer } from '../../mockApi';
|
||||
import { mockGrafanaRulerRule, mockCombinedRule, mockCombinedRuleGroup, grantUserPermissions } from '../../mocks';
|
||||
import { grafanaRulerNamespace, grafanaRulerRule, grafanaRulerGroupName } from '../../mocks/grafanaRulerApi';
|
||||
import { setUpdateRulerRuleNamespaceHandler } from '../../mocks/server/configure';
|
||||
import { captureRequests, serializeRequests } from '../../mocks/server/events';
|
||||
import { getRuleGroupLocationFromCombinedRule } from '../../utils/rules';
|
||||
import { SerializeState } from '../useAsync';
|
||||
|
||||
import { usePauseRuleInGroup } from './usePauseAlertRule';
|
||||
|
||||
setupMswServer();
|
||||
|
||||
beforeAll(() => {
|
||||
setBackendSrv(backendSrv);
|
||||
grantUserPermissions([AccessControlAction.AlertingRuleExternalRead, AccessControlAction.AlertingRuleRead]);
|
||||
});
|
||||
|
||||
describe('pause rule', () => {
|
||||
it('should be able to pause a rule', async () => {
|
||||
const capture = captureRequests();
|
||||
setUpdateRulerRuleNamespaceHandler({ delay: 0 });
|
||||
|
||||
render(<PauseTestComponent />);
|
||||
expect(byText(/uninitialized/i).get()).toBeInTheDocument();
|
||||
|
||||
await userEvent.click(byRole('button').get());
|
||||
expect(await byText(/loading/i).find()).toBeInTheDocument();
|
||||
|
||||
expect(await byText(/success/i).find()).toBeInTheDocument();
|
||||
expect(await byText(/result/i).find()).toBeInTheDocument();
|
||||
expect(byText(/error/i).query()).not.toBeInTheDocument();
|
||||
|
||||
const requests = await capture;
|
||||
const [get, update, ...rest] = await serializeRequests(requests);
|
||||
|
||||
expect(update.body).toHaveProperty('rules[0].grafana_alert.is_paused', true);
|
||||
expect([get, update, ...rest]).toMatchSnapshot();
|
||||
});
|
||||
|
||||
it('should throw if the rule is not found in the group', async () => {
|
||||
setUpdateRulerRuleNamespaceHandler();
|
||||
render(
|
||||
<PauseTestComponent
|
||||
rulerRule={mockGrafanaRulerRule({ uid: 'does-not-exist', namespace_uid: grafanaRulerNamespace.uid })}
|
||||
/>
|
||||
);
|
||||
expect(byText(/uninitialized/i).get()).toBeInTheDocument();
|
||||
|
||||
await userEvent.click(byRole('button').get());
|
||||
expect(await byText(/error: No rule with UID/i).find()).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should be able to handle error', async () => {
|
||||
setUpdateRulerRuleNamespaceHandler({
|
||||
delay: 0,
|
||||
response: new HttpResponse('oops', { status: 500 }),
|
||||
});
|
||||
|
||||
render(<PauseTestComponent />);
|
||||
|
||||
expect(await byText(/uninitialized/i).find()).toBeInTheDocument();
|
||||
|
||||
await userEvent.click(byRole('button').get());
|
||||
expect(await byText(/loading/i).find()).toBeInTheDocument();
|
||||
expect(byText(/success/i).query()).not.toBeInTheDocument();
|
||||
expect(await byText(/error: oops/i).find()).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
// this test component will cycle through the loading states
|
||||
const PauseTestComponent = (options: { rulerRule?: RulerGrafanaRuleDTO }) => {
|
||||
const [requestState, pauseRule] = usePauseRuleInGroup();
|
||||
|
||||
const rulerRule = options.rulerRule ?? grafanaRulerRule;
|
||||
const rule = mockCombinedRule({
|
||||
rulerRule,
|
||||
group: mockCombinedRuleGroup(grafanaRulerGroupName, []),
|
||||
});
|
||||
const ruleGroupID = getRuleGroupLocationFromCombinedRule(rule);
|
||||
|
||||
const onClick = () => {
|
||||
// always handle your errors!
|
||||
pauseRule.execute(ruleGroupID, rulerRule.grafana_alert.uid, true).catch(() => {});
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<button onClick={() => onClick()} />
|
||||
<SerializeState state={requestState} />
|
||||
</>
|
||||
);
|
||||
};
|
@ -0,0 +1,37 @@
|
||||
import { t } from 'i18next';
|
||||
|
||||
import { RuleGroupIdentifier } from 'app/types/unified-alerting';
|
||||
|
||||
import { alertRuleApi } from '../../api/alertRuleApi';
|
||||
import { pauseRuleAction } from '../../reducers/ruler/ruleGroups';
|
||||
import { useAsync } from '../useAsync';
|
||||
|
||||
import { useProduceNewRuleGroup } from './useProduceNewRuleGroup';
|
||||
|
||||
/**
|
||||
* Pause a single rule in a (ruler) group. This hook will ensure that mutations on the rule group are safe and will always
|
||||
* use the latest definition of the ruler group identifier.
|
||||
*/
|
||||
export function usePauseRuleInGroup() {
|
||||
const [produceNewRuleGroup] = useProduceNewRuleGroup();
|
||||
const [upsertRuleGroup] = alertRuleApi.endpoints.upsertRuleGroupForNamespace.useMutation();
|
||||
|
||||
return useAsync(async (ruleGroup: RuleGroupIdentifier, uid: string, pause: boolean) => {
|
||||
const { namespaceName } = ruleGroup;
|
||||
|
||||
const action = pauseRuleAction({ uid, pause });
|
||||
const { newRuleGroupDefinition, rulerConfig } = await produceNewRuleGroup(ruleGroup, action);
|
||||
|
||||
const rulePauseMessage = t('alerting.rules.pause-rule.success', 'Rule evaluation paused');
|
||||
const ruleResumeMessage = t('alerting.rules.resume-rule.success', 'Rule evaluation resumed');
|
||||
|
||||
return upsertRuleGroup({
|
||||
rulerConfig,
|
||||
namespace: namespaceName,
|
||||
payload: newRuleGroupDefinition,
|
||||
requestOptions: {
|
||||
successMessage: pause ? rulePauseMessage : ruleResumeMessage,
|
||||
},
|
||||
}).unwrap();
|
||||
});
|
||||
}
|
@ -0,0 +1,52 @@
|
||||
import { Action } from '@reduxjs/toolkit';
|
||||
|
||||
import { dispatch, getState } from 'app/store/store';
|
||||
import { RuleGroupIdentifier } from 'app/types/unified-alerting';
|
||||
|
||||
import { alertRuleApi } from '../../api/alertRuleApi';
|
||||
import { ruleGroupReducer } from '../../reducers/ruler/ruleGroups';
|
||||
import { fetchRulesSourceBuildInfoAction, getDataSourceRulerConfig } from '../../state/actions';
|
||||
|
||||
/**
|
||||
* Hook for reuse that handles freshly fetching a rule group's definition, applying an action to it,
|
||||
* and then performing the API mutations necessary to persist the change.
|
||||
*
|
||||
* All rule groups changes should ideally be implemented as a wrapper around this hook,
|
||||
* to ensure that we always protect as best we can against accidentally overwriting changes,
|
||||
* and to guard against user concurrency issues.
|
||||
*
|
||||
* @throws
|
||||
*/
|
||||
export function useProduceNewRuleGroup() {
|
||||
const [fetchRuleGroup, requestState] = alertRuleApi.endpoints.getRuleGroupForNamespace.useLazyQuery();
|
||||
|
||||
/**
|
||||
* This function will fetch the latest configuration we have for the rule group, apply a diff to it via a reducer and sends
|
||||
* returns the result.
|
||||
*
|
||||
* The API does not allow operations on a single rule and will always overwrite the existing rule group with the payload.
|
||||
*
|
||||
* ┌─────────────────────────┐ ┌───────────────┐ ┌──────────────────┐
|
||||
* │ fetch latest rule group │─▶│ apply reducer │─▶│ new rule group │
|
||||
* └─────────────────────────┘ └───────────────┘ └──────────────────┘
|
||||
*/
|
||||
const produceNewRuleGroup = async (ruleGroup: RuleGroupIdentifier, action: Action) => {
|
||||
const { dataSourceName, groupName, namespaceName } = ruleGroup;
|
||||
|
||||
// @TODO we should really not work with the redux state (getState) here
|
||||
await dispatch(fetchRulesSourceBuildInfoAction({ rulesSourceName: dataSourceName }));
|
||||
const rulerConfig = getDataSourceRulerConfig(getState, dataSourceName);
|
||||
|
||||
const latestRuleGroupDefinition = await fetchRuleGroup({
|
||||
rulerConfig,
|
||||
namespace: namespaceName,
|
||||
group: groupName,
|
||||
}).unwrap();
|
||||
|
||||
const newRuleGroupDefinition = ruleGroupReducer(latestRuleGroupDefinition, action);
|
||||
|
||||
return { newRuleGroupDefinition, rulerConfig };
|
||||
};
|
||||
|
||||
return [produceNewRuleGroup, requestState] as const;
|
||||
}
|
@ -0,0 +1,166 @@
|
||||
import userEvent from '@testing-library/user-event';
|
||||
import { render } from 'test/test-utils';
|
||||
import { byRole, byText } from 'testing-library-selector';
|
||||
|
||||
import { setBackendSrv } from '@grafana/runtime';
|
||||
import { backendSrv } from 'app/core/services/backend_srv';
|
||||
import { AccessControlAction } from 'app/types';
|
||||
import { RuleGroupIdentifier } from 'app/types/unified-alerting';
|
||||
|
||||
import { setupMswServer } from '../../mockApi';
|
||||
import { grantUserPermissions } from '../../mocks';
|
||||
import { grafanaRulerGroupName2, grafanaRulerGroupName, grafanaRulerNamespace } from '../../mocks/grafanaRulerApi';
|
||||
import { NAMESPACE_2, namespace2, GROUP_1, NAMESPACE_1 } from '../../mocks/mimirRulerApi';
|
||||
import { mimirDataSource } from '../../mocks/server/configure';
|
||||
import { MIMIR_DATASOURCE_UID } from '../../mocks/server/constants';
|
||||
import { captureRequests, serializeRequests } from '../../mocks/server/events';
|
||||
import { GRAFANA_RULES_SOURCE_NAME } from '../../utils/datasource';
|
||||
import { SerializeState } from '../useAsync';
|
||||
|
||||
import { useMoveRuleGroup, useRenameRuleGroup, useUpdateRuleGroupConfiguration } from './useUpdateRuleGroup';
|
||||
|
||||
setupMswServer();
|
||||
|
||||
beforeAll(() => {
|
||||
setBackendSrv(backendSrv);
|
||||
grantUserPermissions([AccessControlAction.AlertingRuleExternalRead, AccessControlAction.AlertingRuleRead]);
|
||||
});
|
||||
|
||||
describe('useUpdateRuleGroupConfiguration', () => {
|
||||
it('should update a rule group interval', async () => {
|
||||
const capture = captureRequests();
|
||||
|
||||
render(<UpdateRuleGroupComponent />);
|
||||
await userEvent.click(byRole('button').get());
|
||||
expect(await byText(/success/i).find()).toBeInTheDocument();
|
||||
|
||||
const requests = await capture;
|
||||
const serializedRequests = await serializeRequests(requests);
|
||||
expect(serializedRequests).toMatchSnapshot();
|
||||
});
|
||||
|
||||
it('should rename a rule group', async () => {
|
||||
const capture = captureRequests();
|
||||
|
||||
render(<RenameRuleGroupComponent />);
|
||||
await userEvent.click(byRole('button').get());
|
||||
expect(await byText(/success/i).find()).toBeInTheDocument();
|
||||
|
||||
const requests = await capture;
|
||||
const serializedRequests = await serializeRequests(requests);
|
||||
expect(serializedRequests).toMatchSnapshot();
|
||||
});
|
||||
|
||||
it('should throw if we are trying to merge rule groups', async () => {
|
||||
render(<RenameRuleGroupComponent group={grafanaRulerGroupName2} />);
|
||||
await userEvent.click(byRole('button').get());
|
||||
expect(await byText(/error:.+not supported.+/i).find()).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should not be able to move a Grafana managed rule group', async () => {
|
||||
render(<MoveGrafanaManagedRuleGroupComponent />);
|
||||
await userEvent.click(byRole('button').get());
|
||||
expect(await byText(/error:.+not supported.+/i).find()).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should be able to move a Data Source managed rule group', async () => {
|
||||
mimirDataSource();
|
||||
const capture = captureRequests();
|
||||
|
||||
render(<MoveDataSourceManagedRuleGroupComponent namespace={NAMESPACE_2} group={'a-new-group'} interval={'2m'} />);
|
||||
await userEvent.click(byRole('button').get());
|
||||
expect(await byText(/success/i).find()).toBeInTheDocument();
|
||||
|
||||
const requests = await capture;
|
||||
const serializedRequests = await serializeRequests(requests);
|
||||
expect(serializedRequests).toMatchSnapshot();
|
||||
});
|
||||
|
||||
it('should not move a Data Source managed rule group to namespace with existing target group name', async () => {
|
||||
mimirDataSource();
|
||||
|
||||
render(
|
||||
<MoveDataSourceManagedRuleGroupComponent namespace={NAMESPACE_2} group={namespace2[0].name} interval={'2m'} />
|
||||
);
|
||||
await userEvent.click(byRole('button').get());
|
||||
expect(await byText(/error:.+not supported.+/i).find()).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
const UpdateRuleGroupComponent = () => {
|
||||
const [requestState, updateRuleGroup] = useUpdateRuleGroupConfiguration();
|
||||
|
||||
const ruleGroupID: RuleGroupIdentifier = {
|
||||
dataSourceName: GRAFANA_RULES_SOURCE_NAME,
|
||||
groupName: grafanaRulerGroupName,
|
||||
namespaceName: grafanaRulerNamespace.uid,
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<button onClick={() => updateRuleGroup.execute(ruleGroupID, '2m')} />
|
||||
<SerializeState state={requestState} />
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
const RenameRuleGroupComponent = ({ group = 'another-group-name' }: { group?: string }) => {
|
||||
const [requestState, renameRuleGroup] = useRenameRuleGroup();
|
||||
|
||||
const ruleGroupID: RuleGroupIdentifier = {
|
||||
dataSourceName: GRAFANA_RULES_SOURCE_NAME,
|
||||
groupName: grafanaRulerGroupName,
|
||||
namespaceName: grafanaRulerNamespace.uid,
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<button onClick={() => renameRuleGroup.execute(ruleGroupID, group, '2m')} />
|
||||
<SerializeState state={requestState} />
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
const MoveGrafanaManagedRuleGroupComponent = () => {
|
||||
const [requestState, moveRuleGroup] = useMoveRuleGroup();
|
||||
|
||||
const ruleGroupID: RuleGroupIdentifier = {
|
||||
dataSourceName: GRAFANA_RULES_SOURCE_NAME,
|
||||
groupName: grafanaRulerGroupName,
|
||||
namespaceName: grafanaRulerNamespace.uid,
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<button onClick={() => moveRuleGroup.execute(ruleGroupID, 'another-namespace', 'another-group-name', '2m')} />
|
||||
<SerializeState state={requestState} />
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
type MoveDataSourceManagedRuleGroupComponentProps = {
|
||||
namespace: string;
|
||||
group: string;
|
||||
interval: string;
|
||||
};
|
||||
|
||||
const MoveDataSourceManagedRuleGroupComponent = ({
|
||||
namespace,
|
||||
group,
|
||||
interval,
|
||||
}: MoveDataSourceManagedRuleGroupComponentProps) => {
|
||||
const [requestState, moveRuleGroup] = useMoveRuleGroup();
|
||||
|
||||
const ruleGroupID: RuleGroupIdentifier = {
|
||||
dataSourceName: MIMIR_DATASOURCE_UID,
|
||||
groupName: GROUP_1,
|
||||
namespaceName: NAMESPACE_1,
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<button onClick={() => moveRuleGroup.execute(ruleGroupID, namespace, group, interval)} />
|
||||
<SerializeState state={requestState} />
|
||||
</>
|
||||
);
|
||||
};
|
@ -1,135 +1,14 @@
|
||||
import { Action } from '@reduxjs/toolkit';
|
||||
import { t } from 'i18next';
|
||||
|
||||
import { t } from 'app/core/internationalization';
|
||||
import { dispatch, getState } from 'app/store/store';
|
||||
import { RuleGroupIdentifier } from 'app/types/unified-alerting';
|
||||
import { RulerRuleDTO } from 'app/types/unified-alerting-dto';
|
||||
|
||||
import { alertRuleApi } from '../api/alertRuleApi';
|
||||
import { notFoundToNullOrThrow } from '../api/util';
|
||||
import {
|
||||
deleteRuleAction,
|
||||
moveRuleGroupAction,
|
||||
pauseRuleAction,
|
||||
renameRuleGroupAction,
|
||||
ruleGroupReducer,
|
||||
updateRuleGroupAction,
|
||||
} from '../reducers/ruler/ruleGroups';
|
||||
import { fetchRulesSourceBuildInfoAction, getDataSourceRulerConfig } from '../state/actions';
|
||||
import { isGrafanaRulesSource } from '../utils/datasource';
|
||||
import { alertRuleApi } from '../../api/alertRuleApi';
|
||||
import { notFoundToNullOrThrow } from '../../api/util';
|
||||
import { updateRuleGroupAction, moveRuleGroupAction, renameRuleGroupAction } from '../../reducers/ruler/ruleGroups';
|
||||
import { isGrafanaRulesSource } from '../../utils/datasource';
|
||||
import { useAsync } from '../useAsync';
|
||||
|
||||
import { useAsync } from './useAsync';
|
||||
|
||||
/**
|
||||
* Hook for reuse that handles freshly fetching a rule group's definition, applying an action to it,
|
||||
* and then performing the API mutations necessary to persist the change.
|
||||
*
|
||||
* All rule groups changes should ideally be implemented as a wrapper around this hook,
|
||||
* to ensure that we always protect as best we can against accidentally overwriting changes,
|
||||
* and to guard against user concurrency issues.
|
||||
*
|
||||
* @throws
|
||||
*/
|
||||
function useProduceNewRuleGroup() {
|
||||
const [fetchRuleGroup, requestState] = alertRuleApi.endpoints.getRuleGroupForNamespace.useLazyQuery();
|
||||
|
||||
/**
|
||||
* This function will fetch the latest configuration we have for the rule group, apply a diff to it via a reducer and sends
|
||||
* returns the result.
|
||||
*
|
||||
* The API does not allow operations on a single rule and will always overwrite the existing rule group with the payload.
|
||||
*
|
||||
* ┌─────────────────────────┐ ┌───────────────┐ ┌──────────────────┐
|
||||
* │ fetch latest rule group │─▶│ apply reducer │─▶│ new rule group │
|
||||
* └─────────────────────────┘ └───────────────┘ └──────────────────┘
|
||||
*/
|
||||
const produceNewRuleGroup = async (ruleGroup: RuleGroupIdentifier, action: Action) => {
|
||||
const { dataSourceName, groupName, namespaceName } = ruleGroup;
|
||||
|
||||
// @TODO we should really not work with the redux state (getState) here
|
||||
await dispatch(fetchRulesSourceBuildInfoAction({ rulesSourceName: dataSourceName }));
|
||||
const rulerConfig = getDataSourceRulerConfig(getState, dataSourceName);
|
||||
|
||||
const latestRuleGroupDefinition = await fetchRuleGroup({
|
||||
rulerConfig,
|
||||
namespace: namespaceName,
|
||||
group: groupName,
|
||||
}).unwrap();
|
||||
|
||||
const newRuleGroupDefinition = ruleGroupReducer(latestRuleGroupDefinition, action);
|
||||
|
||||
return { newRuleGroupDefinition, rulerConfig };
|
||||
};
|
||||
|
||||
return [produceNewRuleGroup, requestState] as const;
|
||||
}
|
||||
|
||||
/**
|
||||
* Pause a single rule in a (ruler) group. This hook will ensure that mutations on the rule group are safe and will always
|
||||
* use the latest definition of the ruler group identifier.
|
||||
*/
|
||||
export function usePauseRuleInGroup() {
|
||||
const [produceNewRuleGroup] = useProduceNewRuleGroup();
|
||||
const [upsertRuleGroup] = alertRuleApi.endpoints.upsertRuleGroupForNamespace.useMutation();
|
||||
|
||||
return useAsync(async (ruleGroup: RuleGroupIdentifier, uid: string, pause: boolean) => {
|
||||
const { namespaceName } = ruleGroup;
|
||||
|
||||
const action = pauseRuleAction({ uid, pause });
|
||||
const { newRuleGroupDefinition, rulerConfig } = await produceNewRuleGroup(ruleGroup, action);
|
||||
|
||||
const rulePauseMessage = t('alerting.rules.pause-rule.success', 'Rule evaluation paused');
|
||||
const ruleResumeMessage = t('alerting.rules.resume-rule.success', 'Rule evaluation resumed');
|
||||
|
||||
return upsertRuleGroup({
|
||||
rulerConfig,
|
||||
namespace: namespaceName,
|
||||
payload: newRuleGroupDefinition,
|
||||
requestOptions: {
|
||||
successMessage: pause ? rulePauseMessage : ruleResumeMessage,
|
||||
},
|
||||
}).unwrap();
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete a single rule from a (ruler) group. This hook will ensure that mutations on the rule group are safe and will always
|
||||
* use the latest definition of the ruler group identifier.
|
||||
*
|
||||
* If no more rules are left in the group it will remove the entire group instead of updating.
|
||||
*/
|
||||
export function useDeleteRuleFromGroup() {
|
||||
const [produceNewRuleGroup] = useProduceNewRuleGroup();
|
||||
const [upsertRuleGroup] = alertRuleApi.endpoints.upsertRuleGroupForNamespace.useMutation();
|
||||
const [deleteRuleGroup] = alertRuleApi.endpoints.deleteRuleGroupFromNamespace.useMutation();
|
||||
|
||||
return useAsync(async (ruleGroup: RuleGroupIdentifier, rule: RulerRuleDTO) => {
|
||||
const { groupName, namespaceName } = ruleGroup;
|
||||
|
||||
const action = deleteRuleAction({ rule });
|
||||
const { newRuleGroupDefinition, rulerConfig } = await produceNewRuleGroup(ruleGroup, action);
|
||||
|
||||
const successMessage = t('alerting.rules.delete-rule.success', 'Rule successfully deleted');
|
||||
|
||||
// if we have no more rules left after reducing, remove the entire group
|
||||
if (newRuleGroupDefinition.rules.length === 0) {
|
||||
return deleteRuleGroup({
|
||||
rulerConfig,
|
||||
namespace: namespaceName,
|
||||
group: groupName,
|
||||
requestOptions: { successMessage },
|
||||
}).unwrap();
|
||||
}
|
||||
|
||||
// otherwise just update the group
|
||||
return upsertRuleGroup({
|
||||
rulerConfig,
|
||||
namespace: namespaceName,
|
||||
payload: newRuleGroupDefinition,
|
||||
requestOptions: { successMessage },
|
||||
}).unwrap();
|
||||
});
|
||||
}
|
||||
import { useProduceNewRuleGroup } from './useProduceNewRuleGroup';
|
||||
|
||||
/**
|
||||
* Update an existing rule group, currently only supports updating the interval.
|
@ -3,6 +3,8 @@
|
||||
*/
|
||||
import { useMemo, useRef, useState } from 'react';
|
||||
|
||||
import { stringifyErrorLike } from '../utils/misc';
|
||||
|
||||
export type AsyncStatus = 'loading' | 'success' | 'error' | 'not-executed';
|
||||
|
||||
export type AsyncState<Result> =
|
||||
@ -179,3 +181,18 @@ export function anyOfRequestState(...states: Array<AsyncState<unknown>>) {
|
||||
success: states.some(isSuccess),
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* This is only used for testing and serializing the async state
|
||||
*/
|
||||
export function SerializeState({ state }: { state: AsyncState<unknown> }) {
|
||||
return (
|
||||
<>
|
||||
{isUninitialized(state) && 'uninitialized'}
|
||||
{isLoading(state) && 'loading'}
|
||||
{isSuccess(state) && 'success'}
|
||||
{isSuccess(state) && `result: ${JSON.stringify(state.result, null, 2)}`}
|
||||
{isError(state) && `error: ${stringifyErrorLike(state.error)}`}
|
||||
</>
|
||||
);
|
||||
}
|
@ -1,399 +0,0 @@
|
||||
import { HttpResponse } from 'msw';
|
||||
import { render, userEvent } from 'test/test-utils';
|
||||
import { byRole, byText } from 'testing-library-selector';
|
||||
|
||||
import { setBackendSrv } from '@grafana/runtime';
|
||||
import { backendSrv } from 'app/core/services/backend_srv';
|
||||
import { AccessControlAction } from 'app/types';
|
||||
import { CombinedRule, RuleGroupIdentifier } from 'app/types/unified-alerting';
|
||||
import { RulerGrafanaRuleDTO } from 'app/types/unified-alerting-dto';
|
||||
|
||||
import { setupMswServer } from '../mockApi';
|
||||
import {
|
||||
grantUserPermissions,
|
||||
mockCombinedRule,
|
||||
mockCombinedRuleGroup,
|
||||
mockGrafanaRulerRule,
|
||||
mockRulerAlertingRule,
|
||||
mockRulerRecordingRule,
|
||||
mockRulerRuleGroup,
|
||||
} from '../mocks';
|
||||
import {
|
||||
grafanaRulerGroupName,
|
||||
grafanaRulerGroupName2,
|
||||
grafanaRulerNamespace,
|
||||
grafanaRulerRule,
|
||||
} from '../mocks/grafanaRulerApi';
|
||||
import { GROUP_1, NAMESPACE_1, NAMESPACE_2, namespace2 } from '../mocks/mimirRulerApi';
|
||||
import {
|
||||
mimirDataSource,
|
||||
setRulerRuleGroupHandler,
|
||||
setUpdateRulerRuleNamespaceHandler,
|
||||
} from '../mocks/server/configure';
|
||||
import { MIMIR_DATASOURCE_UID } from '../mocks/server/constants';
|
||||
import { captureRequests, serializeRequests } from '../mocks/server/events';
|
||||
import { rulerRuleGroupHandler, updateRulerRuleNamespaceHandler } from '../mocks/server/handlers/grafanaRuler';
|
||||
import { GRAFANA_RULES_SOURCE_NAME } from '../utils/datasource';
|
||||
import { stringifyErrorLike } from '../utils/misc';
|
||||
import { getRuleGroupLocationFromCombinedRule } from '../utils/rules';
|
||||
|
||||
import { AsyncState, isError, isLoading, isSuccess, isUninitialized } from './useAsync';
|
||||
import {
|
||||
useDeleteRuleFromGroup,
|
||||
useMoveRuleGroup,
|
||||
usePauseRuleInGroup,
|
||||
useRenameRuleGroup,
|
||||
useUpdateRuleGroupConfiguration,
|
||||
} from './useProduceNewRuleGroup';
|
||||
|
||||
const server = setupMswServer();
|
||||
|
||||
beforeAll(() => {
|
||||
setBackendSrv(backendSrv);
|
||||
grantUserPermissions([AccessControlAction.AlertingRuleExternalRead, AccessControlAction.AlertingRuleRead]);
|
||||
});
|
||||
|
||||
describe('pause rule', () => {
|
||||
it('should be able to pause a rule', async () => {
|
||||
const capture = captureRequests();
|
||||
setUpdateRulerRuleNamespaceHandler({ delay: 0 });
|
||||
|
||||
render(<PauseTestComponent />);
|
||||
expect(byText(/uninitialized/i).get()).toBeInTheDocument();
|
||||
|
||||
await userEvent.click(byRole('button').get());
|
||||
expect(await byText(/loading/i).find()).toBeInTheDocument();
|
||||
|
||||
expect(await byText(/success/i).find()).toBeInTheDocument();
|
||||
expect(await byText(/result/i).find()).toBeInTheDocument();
|
||||
expect(byText(/error/i).query()).not.toBeInTheDocument();
|
||||
|
||||
const requests = await capture;
|
||||
const [get, update, ...rest] = await serializeRequests(requests);
|
||||
|
||||
expect(update.body).toHaveProperty('rules[0].grafana_alert.is_paused', true);
|
||||
expect([get, update, ...rest]).toMatchSnapshot();
|
||||
});
|
||||
|
||||
it('should throw if the rule is not found in the group', async () => {
|
||||
setUpdateRulerRuleNamespaceHandler();
|
||||
render(
|
||||
<PauseTestComponent
|
||||
rulerRule={mockGrafanaRulerRule({ uid: 'does-not-exist', namespace_uid: grafanaRulerNamespace.uid })}
|
||||
/>
|
||||
);
|
||||
expect(byText(/uninitialized/i).get()).toBeInTheDocument();
|
||||
|
||||
await userEvent.click(byRole('button').get());
|
||||
expect(await byText(/error: No rule with UID/i).find()).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should be able to handle error', async () => {
|
||||
setUpdateRulerRuleNamespaceHandler({
|
||||
delay: 0,
|
||||
response: new HttpResponse('oops', { status: 500 }),
|
||||
});
|
||||
|
||||
render(<PauseTestComponent />);
|
||||
|
||||
expect(await byText(/uninitialized/i).find()).toBeInTheDocument();
|
||||
|
||||
await userEvent.click(byRole('button').get());
|
||||
expect(await byText(/loading/i).find()).toBeInTheDocument();
|
||||
expect(byText(/success/i).query()).not.toBeInTheDocument();
|
||||
expect(await byText(/error: oops/i).find()).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
describe('delete rule', () => {
|
||||
it('should be able to delete a Grafana managed rule', async () => {
|
||||
const rules = [
|
||||
mockCombinedRule({
|
||||
name: 'r1',
|
||||
rulerRule: mockGrafanaRulerRule({ uid: 'r1' }),
|
||||
}),
|
||||
mockCombinedRule({
|
||||
name: 'r2',
|
||||
rulerRule: mockGrafanaRulerRule({ uid: 'r2' }),
|
||||
}),
|
||||
];
|
||||
const group = mockRulerRuleGroup({
|
||||
name: 'group-1',
|
||||
rules: [rules[0].rulerRule!, rules[1].rulerRule!],
|
||||
});
|
||||
|
||||
const getGroup = rulerRuleGroupHandler({
|
||||
delay: 0,
|
||||
response: HttpResponse.json(group),
|
||||
});
|
||||
|
||||
const updateNamespace = updateRulerRuleNamespaceHandler({
|
||||
response: new HttpResponse(undefined, { status: 200 }),
|
||||
});
|
||||
|
||||
server.use(getGroup, updateNamespace);
|
||||
|
||||
const capture = captureRequests();
|
||||
|
||||
render(<DeleteTestComponent rule={rules[1]} />);
|
||||
|
||||
await userEvent.click(byRole('button').get());
|
||||
|
||||
expect(await byText(/success/i).find()).toBeInTheDocument();
|
||||
|
||||
const requests = await capture;
|
||||
const serializedRequests = await serializeRequests(requests);
|
||||
expect(serializedRequests).toMatchSnapshot();
|
||||
});
|
||||
|
||||
it('should be able to delete a Data source managed rule', async () => {
|
||||
setUpdateRulerRuleNamespaceHandler({
|
||||
response: new HttpResponse(undefined, { status: 200 }),
|
||||
});
|
||||
|
||||
const rules = [
|
||||
mockCombinedRule({
|
||||
name: 'r1',
|
||||
rulerRule: mockRulerAlertingRule({ alert: 'r1', labels: { foo: 'bar' } }),
|
||||
}),
|
||||
mockCombinedRule({
|
||||
name: 'r2',
|
||||
rulerRule: mockRulerRecordingRule({ record: 'r2', labels: { bar: 'baz' } }),
|
||||
}),
|
||||
];
|
||||
|
||||
const group = mockRulerRuleGroup({
|
||||
name: 'group-1',
|
||||
rules: [rules[0].rulerRule!, rules[1].rulerRule!],
|
||||
});
|
||||
|
||||
setRulerRuleGroupHandler({
|
||||
delay: 0,
|
||||
response: HttpResponse.json(group),
|
||||
});
|
||||
|
||||
const capture = captureRequests();
|
||||
|
||||
render(<DeleteTestComponent rule={rules[1]} />);
|
||||
|
||||
await userEvent.click(byRole('button').get());
|
||||
|
||||
expect(await byText(/success/i).find()).toBeInTheDocument();
|
||||
|
||||
const requests = await capture;
|
||||
const serializedRequests = await serializeRequests(requests);
|
||||
expect(serializedRequests).toMatchSnapshot();
|
||||
});
|
||||
|
||||
it('should delete the entire group if no more rules are left', async () => {
|
||||
const capture = captureRequests();
|
||||
|
||||
const combined = mockCombinedRule({
|
||||
rulerRule: grafanaRulerRule,
|
||||
});
|
||||
|
||||
render(<DeleteTestComponent rule={combined} />);
|
||||
await userEvent.click(byRole('button').get());
|
||||
|
||||
expect(await byText(/success/i).find()).toBeInTheDocument();
|
||||
|
||||
const requests = await capture;
|
||||
const serializedRequests = await serializeRequests(requests);
|
||||
expect(serializedRequests).toMatchSnapshot();
|
||||
});
|
||||
});
|
||||
|
||||
describe('useUpdateRuleGroupConfiguration', () => {
|
||||
it('should update a rule group interval', async () => {
|
||||
const capture = captureRequests();
|
||||
|
||||
render(<UpdateRuleGroupComponent />);
|
||||
await userEvent.click(byRole('button').get());
|
||||
expect(await byText(/success/i).find()).toBeInTheDocument();
|
||||
|
||||
const requests = await capture;
|
||||
const serializedRequests = await serializeRequests(requests);
|
||||
expect(serializedRequests).toMatchSnapshot();
|
||||
});
|
||||
|
||||
it('should rename a rule group', async () => {
|
||||
const capture = captureRequests();
|
||||
|
||||
render(<RenameRuleGroupComponent />);
|
||||
await userEvent.click(byRole('button').get());
|
||||
expect(await byText(/success/i).find()).toBeInTheDocument();
|
||||
|
||||
const requests = await capture;
|
||||
const serializedRequests = await serializeRequests(requests);
|
||||
expect(serializedRequests).toMatchSnapshot();
|
||||
});
|
||||
|
||||
it('should throw if we are trying to merge rule groups', async () => {
|
||||
render(<RenameRuleGroupComponent group={grafanaRulerGroupName2} />);
|
||||
await userEvent.click(byRole('button').get());
|
||||
expect(await byText(/error:.+not supported.+/i).find()).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should not be able to move a Grafana managed rule group', async () => {
|
||||
render(<MoveGrafanaManagedRuleGroupComponent />);
|
||||
await userEvent.click(byRole('button').get());
|
||||
expect(await byText(/error:.+not supported.+/i).find()).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should be able to move a Data Source managed rule group', async () => {
|
||||
mimirDataSource();
|
||||
const capture = captureRequests();
|
||||
|
||||
render(<MoveDataSourceManagedRuleGroupComponent namespace={NAMESPACE_2} group={'a-new-group'} interval={'2m'} />);
|
||||
await userEvent.click(byRole('button').get());
|
||||
expect(await byText(/success/i).find()).toBeInTheDocument();
|
||||
|
||||
const requests = await capture;
|
||||
const serializedRequests = await serializeRequests(requests);
|
||||
expect(serializedRequests).toMatchSnapshot();
|
||||
});
|
||||
|
||||
it('should not move a Data Source managed rule group to namespace with existing target group name', async () => {
|
||||
mimirDataSource();
|
||||
|
||||
render(
|
||||
<MoveDataSourceManagedRuleGroupComponent namespace={NAMESPACE_2} group={namespace2[0].name} interval={'2m'} />
|
||||
);
|
||||
await userEvent.click(byRole('button').get());
|
||||
expect(await byText(/error:.+not supported.+/i).find()).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
const UpdateRuleGroupComponent = () => {
|
||||
const [requestState, updateRuleGroup] = useUpdateRuleGroupConfiguration();
|
||||
|
||||
const ruleGroupID: RuleGroupIdentifier = {
|
||||
dataSourceName: GRAFANA_RULES_SOURCE_NAME,
|
||||
groupName: grafanaRulerGroupName,
|
||||
namespaceName: grafanaRulerNamespace.uid,
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<button onClick={() => updateRuleGroup.execute(ruleGroupID, '2m')} />
|
||||
<SerializeState state={requestState} />
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
const RenameRuleGroupComponent = ({ group = 'another-group-name' }: { group?: string }) => {
|
||||
const [requestState, renameRuleGroup] = useRenameRuleGroup();
|
||||
|
||||
const ruleGroupID: RuleGroupIdentifier = {
|
||||
dataSourceName: GRAFANA_RULES_SOURCE_NAME,
|
||||
groupName: grafanaRulerGroupName,
|
||||
namespaceName: grafanaRulerNamespace.uid,
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<button onClick={() => renameRuleGroup.execute(ruleGroupID, group, '2m')} />
|
||||
<SerializeState state={requestState} />
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
const MoveGrafanaManagedRuleGroupComponent = () => {
|
||||
const [requestState, moveRuleGroup] = useMoveRuleGroup();
|
||||
|
||||
const ruleGroupID: RuleGroupIdentifier = {
|
||||
dataSourceName: GRAFANA_RULES_SOURCE_NAME,
|
||||
groupName: grafanaRulerGroupName,
|
||||
namespaceName: grafanaRulerNamespace.uid,
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<button onClick={() => moveRuleGroup.execute(ruleGroupID, 'another-namespace', 'another-group-name', '2m')} />
|
||||
<SerializeState state={requestState} />
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
function SerializeState({ state }: { state: AsyncState<unknown> }) {
|
||||
return (
|
||||
<>
|
||||
{isUninitialized(state) && 'uninitialized'}
|
||||
{isLoading(state) && 'loading'}
|
||||
{isSuccess(state) && 'success'}
|
||||
{isSuccess(state) && `result: ${JSON.stringify(state.result, null, 2)}`}
|
||||
{isError(state) && `error: ${stringifyErrorLike(state.error)}`}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
type MoveDataSourceManagedRuleGroupComponentProps = {
|
||||
namespace: string;
|
||||
group: string;
|
||||
interval: string;
|
||||
};
|
||||
|
||||
const MoveDataSourceManagedRuleGroupComponent = ({
|
||||
namespace,
|
||||
group,
|
||||
interval,
|
||||
}: MoveDataSourceManagedRuleGroupComponentProps) => {
|
||||
const [requestState, moveRuleGroup] = useMoveRuleGroup();
|
||||
|
||||
const ruleGroupID: RuleGroupIdentifier = {
|
||||
dataSourceName: MIMIR_DATASOURCE_UID,
|
||||
groupName: GROUP_1,
|
||||
namespaceName: NAMESPACE_1,
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<button onClick={() => moveRuleGroup.execute(ruleGroupID, namespace, group, interval)} />
|
||||
<SerializeState state={requestState} />
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
// this test component will cycle through the loading states
|
||||
const PauseTestComponent = (options: { rulerRule?: RulerGrafanaRuleDTO }) => {
|
||||
const [requestState, pauseRule] = usePauseRuleInGroup();
|
||||
|
||||
const rulerRule = options.rulerRule ?? grafanaRulerRule;
|
||||
const rule = mockCombinedRule({
|
||||
rulerRule,
|
||||
group: mockCombinedRuleGroup(grafanaRulerGroupName, []),
|
||||
});
|
||||
const ruleGroupID = getRuleGroupLocationFromCombinedRule(rule);
|
||||
|
||||
const onClick = () => {
|
||||
// always handle your errors!
|
||||
pauseRule.execute(ruleGroupID, rulerRule.grafana_alert.uid, true).catch(() => {});
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<button onClick={() => onClick()} />
|
||||
<SerializeState state={requestState} />
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
type DeleteTestComponentProps = {
|
||||
rule: CombinedRule;
|
||||
};
|
||||
const DeleteTestComponent = ({ rule }: DeleteTestComponentProps) => {
|
||||
const [requestState, deleteRuleFromGroup] = useDeleteRuleFromGroup();
|
||||
|
||||
// always handle your errors!
|
||||
const ruleGroupID = getRuleGroupLocationFromCombinedRule(rule);
|
||||
const onClick = () => {
|
||||
deleteRuleFromGroup.execute(ruleGroupID, rule.rulerRule!);
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<button onClick={() => onClick()} />
|
||||
<SerializeState state={requestState} />
|
||||
</>
|
||||
);
|
||||
};
|
Loading…
Reference in New Issue
Block a user