Alerting: Re-organise rule group hooks (#90368)
This commit is contained in:
@@ -7,8 +7,8 @@ import {
|
|||||||
} from 'app/features/alerting/unified/utils/rules';
|
} from 'app/features/alerting/unified/utils/rules';
|
||||||
import { CombinedRule } from 'app/types/unified-alerting';
|
import { CombinedRule } from 'app/types/unified-alerting';
|
||||||
|
|
||||||
|
import { usePauseRuleInGroup } from '../hooks/ruleGroup/usePauseAlertRule';
|
||||||
import { isLoading } from '../hooks/useAsync';
|
import { isLoading } from '../hooks/useAsync';
|
||||||
import { usePauseRuleInGroup } from '../hooks/useProduceNewRuleGroup';
|
|
||||||
import { stringifyErrorLike } from '../utils/misc';
|
import { stringifyErrorLike } from '../utils/misc';
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
|
|||||||
+1
-1
@@ -27,7 +27,7 @@ import {
|
|||||||
trackAlertRuleFormCancelled,
|
trackAlertRuleFormCancelled,
|
||||||
trackAlertRuleFormSaved,
|
trackAlertRuleFormSaved,
|
||||||
} from '../../../Analytics';
|
} from '../../../Analytics';
|
||||||
import { useDeleteRuleFromGroup } from '../../../hooks/useProduceNewRuleGroup';
|
import { useDeleteRuleFromGroup } from '../../../hooks/ruleGroup/useDeleteRuleFromGroup';
|
||||||
import { useUnifiedAlertingSelector } from '../../../hooks/useUnifiedAlertingSelector';
|
import { useUnifiedAlertingSelector } from '../../../hooks/useUnifiedAlertingSelector';
|
||||||
import { saveRuleFormAction } from '../../../state/actions';
|
import { saveRuleFormAction } from '../../../state/actions';
|
||||||
import { RuleFormType, RuleFormValues } from '../../../types/rule-form';
|
import { RuleFormType, RuleFormValues } from '../../../types/rule-form';
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ import { ConfirmModal } from '@grafana/ui';
|
|||||||
import { dispatch } from 'app/store/store';
|
import { dispatch } from 'app/store/store';
|
||||||
import { CombinedRule } from 'app/types/unified-alerting';
|
import { CombinedRule } from 'app/types/unified-alerting';
|
||||||
|
|
||||||
import { useDeleteRuleFromGroup } from '../../hooks/useProduceNewRuleGroup';
|
import { useDeleteRuleFromGroup } from '../../hooks/ruleGroup/useDeleteRuleFromGroup';
|
||||||
import { fetchPromAndRulerRulesAction } from '../../state/actions';
|
import { fetchPromAndRulerRulesAction } from '../../state/actions';
|
||||||
import { getRuleGroupLocationFromCombinedRule } from '../../utils/rules';
|
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 { CombinedRuleGroup, CombinedRuleNamespace, RuleGroupIdentifier } from 'app/types/unified-alerting';
|
||||||
import { RulerRuleDTO } from 'app/types/unified-alerting-dto';
|
import { RulerRuleDTO } from 'app/types/unified-alerting-dto';
|
||||||
|
|
||||||
import { anyOfRequestState } from '../../hooks/useAsync';
|
|
||||||
import {
|
import {
|
||||||
useMoveRuleGroup,
|
|
||||||
useRenameRuleGroup,
|
|
||||||
useUpdateRuleGroupConfiguration,
|
useUpdateRuleGroupConfiguration,
|
||||||
} from '../../hooks/useProduceNewRuleGroup';
|
useRenameRuleGroup,
|
||||||
|
useMoveRuleGroup,
|
||||||
|
} from '../../hooks/ruleGroup/useUpdateRuleGroup';
|
||||||
|
import { anyOfRequestState } from '../../hooks/useAsync';
|
||||||
import { fetchRulerRulesAction, rulesInSameGroupHaveInvalidFor } from '../../state/actions';
|
import { fetchRulerRulesAction, rulesInSameGroupHaveInvalidFor } from '../../state/actions';
|
||||||
import { checkEvaluationIntervalGlobalLimit } from '../../utils/config';
|
import { checkEvaluationIntervalGlobalLimit } from '../../utils/config';
|
||||||
import { getRulesSourceName, GRAFANA_RULES_SOURCE_NAME } from '../../utils/datasource';
|
import { getRulesSourceName, GRAFANA_RULES_SOURCE_NAME } from '../../utils/datasource';
|
||||||
|
|||||||
+156
@@ -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",
|
||||||
|
},
|
||||||
|
]
|
||||||
|
`;
|
||||||
+88
@@ -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",
|
||||||
|
},
|
||||||
|
]
|
||||||
|
`;
|
||||||
-242
@@ -1,247 +1,5 @@
|
|||||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
// 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`] = `
|
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} />
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
+7
-128
@@ -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 { RuleGroupIdentifier } from 'app/types/unified-alerting';
|
||||||
import { RulerRuleDTO } from 'app/types/unified-alerting-dto';
|
|
||||||
|
|
||||||
import { alertRuleApi } from '../api/alertRuleApi';
|
import { alertRuleApi } from '../../api/alertRuleApi';
|
||||||
import { notFoundToNullOrThrow } from '../api/util';
|
import { notFoundToNullOrThrow } from '../../api/util';
|
||||||
import {
|
import { updateRuleGroupAction, moveRuleGroupAction, renameRuleGroupAction } from '../../reducers/ruler/ruleGroups';
|
||||||
deleteRuleAction,
|
import { isGrafanaRulesSource } from '../../utils/datasource';
|
||||||
moveRuleGroupAction,
|
import { useAsync } from '../useAsync';
|
||||||
pauseRuleAction,
|
|
||||||
renameRuleGroupAction,
|
|
||||||
ruleGroupReducer,
|
|
||||||
updateRuleGroupAction,
|
|
||||||
} from '../reducers/ruler/ruleGroups';
|
|
||||||
import { fetchRulesSourceBuildInfoAction, getDataSourceRulerConfig } from '../state/actions';
|
|
||||||
import { isGrafanaRulesSource } from '../utils/datasource';
|
|
||||||
|
|
||||||
import { useAsync } from './useAsync';
|
import { useProduceNewRuleGroup } from './useProduceNewRuleGroup';
|
||||||
|
|
||||||
/**
|
|
||||||
* 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();
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Update an existing rule group, currently only supports updating the interval.
|
* Update an existing rule group, currently only supports updating the interval.
|
||||||
+17
@@ -3,6 +3,8 @@
|
|||||||
*/
|
*/
|
||||||
import { useMemo, useRef, useState } from 'react';
|
import { useMemo, useRef, useState } from 'react';
|
||||||
|
|
||||||
|
import { stringifyErrorLike } from '../utils/misc';
|
||||||
|
|
||||||
export type AsyncStatus = 'loading' | 'success' | 'error' | 'not-executed';
|
export type AsyncStatus = 'loading' | 'success' | 'error' | 'not-executed';
|
||||||
|
|
||||||
export type AsyncState<Result> =
|
export type AsyncState<Result> =
|
||||||
@@ -179,3 +181,18 @@ export function anyOfRequestState(...states: Array<AsyncState<unknown>>) {
|
|||||||
success: states.some(isSuccess),
|
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} />
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
Reference in New Issue
Block a user