diff --git a/public/app/features/alerting/unified/components/rules/RulesGroup.test.tsx b/public/app/features/alerting/unified/components/rules/RulesGroup.test.tsx new file mode 100644 index 00000000000..a9eafa81ed3 --- /dev/null +++ b/public/app/features/alerting/unified/components/rules/RulesGroup.test.tsx @@ -0,0 +1,111 @@ +import { render } from '@testing-library/react'; +import userEvent from '@testing-library/user-event'; +import { configureStore } from 'app/store/configureStore'; +import { CombinedRuleGroup, CombinedRuleNamespace } from 'app/types/unified-alerting'; +import React from 'react'; +import { Provider } from 'react-redux'; +import { byTestId, byText } from 'testing-library-selector'; +import { mockCombinedRule, mockDataSource } from '../../mocks'; +import { RulesGroup } from './RulesGroup'; + +const hasRulerMock = jest.fn(); +jest.mock('../../hooks/useHasRuler', () => ({ + useHasRuler: () => hasRulerMock, +})); + +beforeEach(() => hasRulerMock.mockReset()); + +const ui = { + editGroupButton: byTestId('edit-group'), + deleteGroupButton: byTestId('delete-group'), + confirmDeleteModal: { + header: byText('Delete group'), + confirmButton: byText('Delete'), + }, +}; + +describe('Rules group tests', () => { + const store = configureStore(); + + function renderRulesGroup(namespace: CombinedRuleNamespace, group: CombinedRuleGroup) { + return render( + + + + ); + } + + describe('When the datasource is grafana', () => { + const group: CombinedRuleGroup = { + name: 'TestGroup', + rules: [mockCombinedRule()], + }; + + const namespace: CombinedRuleNamespace = { + name: 'TestNamespace', + rulesSource: 'grafana', + groups: [group], + }; + + it('Should hide delete and edit group buttons', () => { + // Act + renderRulesGroup(namespace, group); + + // Assert + expect(ui.deleteGroupButton.query()).not.toBeInTheDocument(); + expect(ui.editGroupButton.query()).not.toBeInTheDocument(); + }); + }); + + describe('When the datasource is not grafana', () => { + const group: CombinedRuleGroup = { + name: 'TestGroup', + rules: [mockCombinedRule()], + }; + + const namespace: CombinedRuleNamespace = { + name: 'TestNamespace', + rulesSource: mockDataSource(), + groups: [group], + }; + + it('When ruler enabled should display delete and edit group buttons', () => { + // Arrange + hasRulerMock.mockReturnValue(true); + + // Act + renderRulesGroup(namespace, group); + + // Assert + expect(hasRulerMock).toHaveBeenCalled(); + expect(ui.deleteGroupButton.get()).toBeInTheDocument(); + expect(ui.editGroupButton.get()).toBeInTheDocument(); + }); + + it('When ruler disabled should hide delete and edit group buttons', () => { + // Arrange + hasRulerMock.mockReturnValue(false); + + // Act + renderRulesGroup(namespace, group); + + // Assert + expect(hasRulerMock).toHaveBeenCalled(); + expect(ui.deleteGroupButton.query()).not.toBeInTheDocument(); + expect(ui.editGroupButton.query()).not.toBeInTheDocument(); + }); + + it('Delete button click should display confirmation modal', () => { + // Arrange + hasRulerMock.mockReturnValue(true); + + // Act + renderRulesGroup(namespace, group); + userEvent.click(ui.deleteGroupButton.get()); + + // Assert + expect(ui.confirmDeleteModal.header.get()).toBeInTheDocument(); + expect(ui.confirmDeleteModal.confirmButton.get()).toBeInTheDocument(); + }); + }); +}); diff --git a/public/app/features/alerting/unified/components/rules/RulesGroup.tsx b/public/app/features/alerting/unified/components/rules/RulesGroup.tsx index ab267eeb172..04c61de0f57 100644 --- a/public/app/features/alerting/unified/components/rules/RulesGroup.tsx +++ b/public/app/features/alerting/unified/components/rules/RulesGroup.tsx @@ -1,18 +1,21 @@ -import { CombinedRuleGroup, CombinedRuleNamespace } from 'app/types/unified-alerting'; -import React, { FC, useState, useEffect } from 'react'; -import { HorizontalGroup, Icon, Spinner, Tooltip, useStyles2 } from '@grafana/ui'; -import { GrafanaTheme2 } from '@grafana/data'; import { css } from '@emotion/css'; +import { GrafanaTheme2 } from '@grafana/data'; +import { ConfirmModal, HorizontalGroup, Icon, Spinner, Tooltip, useStyles2 } from '@grafana/ui'; +import kbn from 'app/core/utils/kbn'; +import { CombinedRuleGroup, CombinedRuleNamespace } from 'app/types/unified-alerting'; +import pluralize from 'pluralize'; +import React, { FC, useEffect, useState } from 'react'; +import { useDispatch } from 'react-redux'; +import { useFolder } from '../../hooks/useFolder'; +import { useHasRuler } from '../../hooks/useHasRuler'; +import { deleteRulesGroupAction } from '../../state/actions'; +import { GRAFANA_RULES_SOURCE_NAME, isCloudRulesSource } from '../../utils/datasource'; import { isGrafanaRulerRule } from '../../utils/rules'; import { CollapseToggle } from '../CollapseToggle'; -import { RulesTable } from './RulesTable'; -import { GRAFANA_RULES_SOURCE_NAME, isCloudRulesSource } from '../../utils/datasource'; import { ActionIcon } from './ActionIcon'; -import { useHasRuler } from '../../hooks/useHasRuler'; -import kbn from 'app/core/utils/kbn'; -import { useFolder } from '../../hooks/useFolder'; -import { RuleStats } from './RuleStats'; import { EditCloudGroupModal } from './EditCloudGroupModal'; +import { RulesTable } from './RulesTable'; +import { RuleStats } from './RuleStats'; interface Props { namespace: CombinedRuleNamespace; @@ -22,9 +25,11 @@ interface Props { export const RulesGroup: FC = React.memo(({ group, namespace, expandAll }) => { const { rulesSource } = namespace; + const dispatch = useDispatch(); const styles = useStyles2(getStyles); const [isEditingGroup, setIsEditingGroup] = useState(false); + const [isDeletingGroup, setIsDeletingGroup] = useState(false); const [isCollapsed, setIsCollapsed] = useState(!expandAll); useEffect(() => { @@ -39,6 +44,11 @@ export const RulesGroup: FC = React.memo(({ group, namespace, expandAll } // group "is deleting" if rules source has ruler, but this group has no rules that are in ruler const isDeleting = hasRuler(rulesSource) && !group.rules.find((rule) => !!rule.rulerRule); + const deleteGroup = () => { + dispatch(deleteRulesGroupAction(namespace, group)); + setIsDeletingGroup(false); + }; + const actionIcons: React.ReactNode[] = []; // for grafana, link to folder views @@ -88,6 +98,17 @@ export const RulesGroup: FC = React.memo(({ group, namespace, expandAll } onClick={() => setIsEditingGroup(true)} /> ); + + actionIcons.push( + setIsDeletingGroup(true)} + /> + ); } const groupName = isCloudRulesSource(rulesSource) ? `${namespace.name} > ${group.name}` : namespace.name; @@ -129,6 +150,22 @@ export const RulesGroup: FC = React.memo(({ group, namespace, expandAll } {isEditingGroup && ( setIsEditingGroup(false)} /> )} + + Deleting this group will permanently remove the group +
+ and {group.rules.length} alert {pluralize('rule', group.rules.length)} belonging to it. +
+ Are you sure you want to delete this group? + + } + onConfirm={deleteGroup} + onDismiss={() => setIsDeletingGroup(false)} + confirmText="Delete" + /> ); }); diff --git a/public/app/features/alerting/unified/state/actions.ts b/public/app/features/alerting/unified/state/actions.ts index 38e4a4fdb40..c8c5113ef4a 100644 --- a/public/app/features/alerting/unified/state/actions.ts +++ b/public/app/features/alerting/unified/state/actions.ts @@ -12,7 +12,14 @@ import { TestReceiversAlert, } from 'app/plugins/datasource/alertmanager/types'; import { FolderDTO, NotifierDTO, ThunkResult } from 'app/types'; -import { RuleIdentifier, RuleNamespace, RuleWithLocation, StateHistoryItem } from 'app/types/unified-alerting'; +import { + CombinedRuleGroup, + CombinedRuleNamespace, + RuleIdentifier, + RuleNamespace, + RuleWithLocation, + StateHistoryItem, +} from 'app/types/unified-alerting'; import { PostableRulerRuleGroupDTO, RulerGrafanaRuleDTO, @@ -49,6 +56,7 @@ import { import { RuleFormType, RuleFormValues } from '../types/rule-form'; import { getAllRulesSourceNames, + getRulesSourceName, GRAFANA_RULES_SOURCE_NAME, isGrafanaRulesSource, isVanillaPrometheusAlertManagerDataSource, @@ -263,6 +271,24 @@ async function deleteRule(ruleWithLocation: RuleWithLocation): Promise { }); } +export function deleteRulesGroupAction( + namespace: CombinedRuleNamespace, + ruleGroup: CombinedRuleGroup +): ThunkResult { + return async (dispatch) => { + withAppEvents( + (async () => { + const sourceName = getRulesSourceName(namespace.rulesSource); + + await deleteRulerRulesGroup(sourceName, namespace.name, ruleGroup.name); + dispatch(fetchRulerRulesAction({ rulesSourceName: sourceName })); + dispatch(fetchPromRulesAction({ rulesSourceName: sourceName })); + })(), + { successMessage: 'Group deleted' } + ); + }; +} + export function deleteRuleAction( ruleIdentifier: RuleIdentifier, options: { navigateTo?: string } = {}