Alerting: Re-organise rule group hooks (#90368)

This commit is contained in:
Gilles De Mey 2024-07-12 16:33:37 +02:00 committed by GitHub
parent 8400b54a53
commit e64ef2245c
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
16 changed files with 833 additions and 776 deletions

View File

@ -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 {

View File

@ -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';

View File

@ -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';

View File

@ -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';

View File

@ -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",
},
]
`;

View File

@ -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",
},
]
`;

View File

@ -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`] = `
[
{

View File

@ -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} />
</>
);
};

View File

@ -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();
});
}

View File

@ -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} />
</>
);
};

View File

@ -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();
});
}

View File

@ -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;
}

View File

@ -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} />
</>
);
};

View File

@ -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.

View File

@ -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)}`}
</>
);
}

View File

@ -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} />
</>
);
};